Large (>16KB) tabs snapshots are misclassified as frames and dropped
Surfaced by the nightly code-quality survey.
In agent-browser-connection.ts, handleMessage routes stream messages purely by size: any string longer than FRAME_PULSE_THRESHOLD (16384) is treated as a frame. The comment at L8-L10 states the assumption directly: "Stream messages above this size are frames (a base64 JPEG); status/tabs are small JSON control messages."
But a tabs message carries url + title per tab, so a user with many open tabs (long URLs/titles) can push a tabs snapshot past 16KB. When that happens the tab update is silently lost:
- If
wantFrameData() is false (the idle hot path), the payload is never parsed — L273-L274 hash the raw envelope and emit a bare frame-pulse. The tabs never reach handleTabs.
- If
wantFrameData() is true, it parses, sees msg.type === 'tabs' (not 'frame'), falls out of the inner if without returning, and hits the same L273-L274 fallback — again dropping the tabs.
Because the daemon re-broadcasts tabs on its heartbeat, a user whose tab list consistently exceeds the threshold would see the tab list / active-tab selection never update until an unrelated smaller snapshot happens to arrive.
Why this is filed rather than fixed directly
The clean fix depends on the daemon's wire contract, which I can't determine from the client alone:
- Can control messages (
tabs/status) legitimately exceed 16384 bytes? If the daemon guarantees they can't, the current size-as-discriminator is fine and this is a non-issue.
- Is the frame envelope's shape stable enough for a cheap structural discriminator (e.g. peeking for a
"type":"frame" prefix) so large control messages can be routed to normal dispatch without paying a full JSON.parse of ~100KB frames at ~20Hz?
- Should the threshold instead gate only actual frames, with control messages always dispatched regardless of size?
Each of these is a protocol decision. The performance intent of the threshold (avoid parsing large frame JSON on the idle path — see L256-L260) must be preserved, so the fix shouldn't just "parse everything."
Suggested directions
- When
wantFrameData() is true and the parsed large message is a known control type (tabs/status), dispatch it through the normal path instead of dropping it — fixes the hover case with no hot-path cost.
- For the idle case, add a cheap frame discriminator (structural prefix check) so a large non-frame message is routed to control handling without a full parse.
- Whatever is chosen, add a regression test for a
tabs payload that crosses FRAME_PULSE_THRESHOLD.
Severity: low-to-medium — real correctness bug, but gated on a large tab count.
Large (>16KB)
tabssnapshots are misclassified as frames and droppedSurfaced by the nightly code-quality survey.
In
agent-browser-connection.ts,handleMessageroutes stream messages purely by size: any string longer thanFRAME_PULSE_THRESHOLD(16384) is treated as a frame. The comment at L8-L10 states the assumption directly: "Stream messages above this size are frames (a base64 JPEG); status/tabs are small JSON control messages."But a
tabsmessage carriesurl+titleper tab, so a user with many open tabs (long URLs/titles) can push atabssnapshot past 16KB. When that happens the tab update is silently lost:wantFrameData()is false (the idle hot path), the payload is never parsed — L273-L274 hash the raw envelope and emit a bareframe-pulse. The tabs never reachhandleTabs.wantFrameData()is true, it parses, seesmsg.type === 'tabs'(not'frame'), falls out of the innerifwithout returning, and hits the same L273-L274 fallback — again dropping the tabs.Because the daemon re-broadcasts tabs on its heartbeat, a user whose tab list consistently exceeds the threshold would see the tab list / active-tab selection never update until an unrelated smaller snapshot happens to arrive.
Why this is filed rather than fixed directly
The clean fix depends on the daemon's wire contract, which I can't determine from the client alone:
tabs/status) legitimately exceed 16384 bytes? If the daemon guarantees they can't, the current size-as-discriminator is fine and this is a non-issue."type":"frame"prefix) so large control messages can be routed to normal dispatch without paying a fullJSON.parseof ~100KB frames at ~20Hz?Each of these is a protocol decision. The performance intent of the threshold (avoid parsing large frame JSON on the idle path — see L256-L260) must be preserved, so the fix shouldn't just "parse everything."
Suggested directions
wantFrameData()is true and the parsed large message is a known control type (tabs/status), dispatch it through the normal path instead of dropping it — fixes the hover case with no hot-path cost.tabspayload that crossesFRAME_PULSE_THRESHOLD.Severity: low-to-medium — real correctness bug, but gated on a large tab count.