Console stream freezes when an SSE frame payload is the bare null literal
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/versionreturns2.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:
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:
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:
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: nullEvery 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.
Source: agentscope-ai/QwenPaw