Skip to content

useAgentChat reconnect-driven resume races AI SDK Chat.makeRequest finalizer → "Cannot read properties of undefined (reading 'state')" #1837

Description

@rwdaigle

Describe the bug

With useAgentChat({ resume: true }), the SDK calls resumeStream() from its WebSocket onAgentOpen handler on every reconnect. When the socket reconnects repeatedly while a turn is in flight (flaky/mobile links, or a backend redeploy that bounces the Durable Object), overlapping resumes interleave and trip a race in the underlying AI SDK's Chat.makeRequest finalizer, throwing a handled browser TypeError: Cannot read properties of undefined (reading 'state').

The finally block in Chat.makeRequest (Vercel ai) finalizes against the shared mutable this.activeResponse and then clears it:

} finally {
  this.onFinish?.call(this, {
    message: this.activeResponse.state.message,           // bare read — unguarded
    ...
    finishReason: this.activeResponse?.state.finishReason // adjacent field IS optional-chained
  });
  this.activeResponse = void 0;
}

If a second resume starts/settles before the first's finally runs, the first reads this.activeResponse.state after it's been cleared/replaced → the throw. The guard is half-applied: finishReason uses this.activeResponse?.state…, but message: this.activeResponse.state.message is a bare read. The error aborts the resume that was meant to recover the stream, so onFinish may not fire and the chat status can be left unsettled.

To Reproduce

  1. Mount useAgentChat({ resume: true }) and start a streaming turn.
  2. Drop and re-establish the WebSocket repeatedly while the turn is in flight (redeploy the worker / toggle the network) so onAgentOpen fires several resumeStream() calls in quick succession.
  3. Observe TypeError: Cannot read properties of undefined (reading 'state') from the AI SDK Chat.makeRequest finalizer.

Expected behavior

Overlapping reconnect-driven resumes should not throw; a resume that loses the race should settle cleanly without leaving the chat status unsettled.

Version: agents 0.17.0 / @cloudflare/ai-chat 0.9.0 (with ai 6.0.191).

Suggested resolution

Make the Chat.makeRequest finalizer request-local: capture the activeResponse at the start of the request and have onFinish read from that local, clearing this.activeResponse only if it still owns it (mirroring the optional-chaining already on the adjacent finishReason field would, at minimum, stop the throw). The bare read lives in the Vercel ai package, so the definitive fix may be upstream — but since the reconnect-driven resumeStream() is what produces the overlap, serializing resumes here (don't issue a new resumeStream() while one is in flight) would also avoid the interleaving. Happy to send a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions