#7813·QwenPaw

Console stream freezes when an SSE frame payload is the bare null literal

Author: wjt0321Created Sep 16, 2026Updated Sep 17, 2026

Problem

A single malformed SSE frame whose payload is the bare JSON literal null takes down the whole streaming turn in the Console. The exception is thrown inside the stream-consuming generator, caught by an outer handler and only logged, so nothing terminates the response client-side. The UI is left stuck in a "generating" state with no way to recover except reloading the page.

This is a small, self-contained frontend robustness gap, but the user-visible effect is a total chat freeze, so it seems worth fixing independently of where the bad frame comes from.

Environment

  • QwenPaw 2.2.1, Windows desktop build (/api/version returns 2.2.1, so the release is confirmed)
  • Console reached over 127.0.0.1, default webview shell
  • Reproduces across reloads; a hard refresh recovers the session

Observed error

Three occurrences of this in a single Console session log, all with an identical stack:

index-<hash>.js:200 TypeError: Cannot read properties of null (reading 'object')
    at e.value (ui-vendor-<hash>.js:4194:4339)
    at ui-vendor-<hash>.js:4194:16571
    at f (ui-vendor-<hash>.js:4194:7061)
    at Generator.<anonymous> (ui-vendor-<hash>.js:4194:8393)
    at Generator.next (ui-vendor-<hash>.js:4194:7486)
    at <asyncGeneratorStep> (ui-vendor-<hash>.js:4194:12958)
    at o (ui-vendor-<hash>.js:4194:13156)

The error is printed by QwenPaw's own bundle (index-<hash>.js:200 is the console.error call), which is why the failure is silent rather than showing an error page.

Crash site

Column 4339 of that frame resolves to the first statement of the response builder's handle method, in the vendored @agentscope-ai/chat chunk. Pinned in console/package.json at 1.1.73-beta.1787638407498, the method looks like this:

javascript
function handle(data) {
  if (data.object === 'response') {
    this.handleResponse(data);
  } else if (data.object === 'message') {
    if (data.type === AgentScopeRuntimeMessageType.HEARTBEAT) return this.data;
    this.handleMessage(data);
  } else if (data.object === 'content') {
    this.handleContent(data);
  } else {
    this.handleError(data);
  }
  return this.data;
}

There is no null check on data before the first .object access. In the bundle this compiles to function(n){if(n.object==="response")...}, which is exactly the instruction the stack points at.

The caller passes the result of the frame parser straight through, with no validation in between:

javascript
const parsed = (options.responseParser || JSON.parse)(frame.data);
const response = builder.handle(parsed);

Important detail: JSON.parse only returns null when the input is the literal null. Any other malformed payload throws inside JSON.parse instead, and that path is already handled. So a bare null frame is the one input that reaches handle as null rather than throwing earlier.

Why it freezes the UI rather than just dropping a frame

The throw happens inside the generator that iterates the SSE stream, at the step boundary of the parser call, so it unwinds the whole consuming loop. Since no terminal completed / failed state is produced afterwards, the response stays in a generating state forever.

Suggested direction

Guarding the consumer side is the cheapest fix, and it matches a convention the codebase already uses. console/src/pages/Chat/index.tsx already injects a responseParser, and that parser already returns null as a deliberate "ignore this frame" signal for turn_usage, rate_limited, and replay_end. A bare null payload can use the same signal:

typescript
responseParser: (chunk: string) => {
  const payload = JSON.parse(chunk) as Record<string, unknown>;
  if (!payload) return null;   // ignore malformed/empty frame
  ...

Adding an equivalent guard in AgentScopeRuntimeResponseBuilder.handle would be more thorough, but that lives in the @agentscope-ai/chat package rather than this repository.

A related observation, unconfirmed

In the same session log, this warning appeared 52 times:

Message not found for content: null

Every occurrence has msg_id equal to null, meaning content delta events arrived that could not be associated with any message in the output list. I have not established whether this shares a cause with the null frame above. I mention it only so it is on record; I deliberately did not try to bind orphaned content to the current message, since that risks mixing messages across turns or sessions.

Scope

I have not been able to capture the raw SSE frame that produced the null, so I cannot say which event type triggers it or whether it is specific to my configuration. The crash mechanism described above is confirmed by the stack trace and by the source; the origin of the frame is not. A separate report covers the backend side.