Good news — the keep-alive is working (you got an error instead of a silent freeze). The poll loop ran all 100 polls without detecting completion, which means the button state detection is failing to recognise the waveform icon.
The waveform has multiple <path> elements and no <rect>, but so does the arrow — we’re distinguishing them by pathCount >= 4. That threshold might be wrong for your browser/Claude version.
Let me add the actual path count to the poll log so we can see what number it’s returning, and also broaden the detection to not rely on a specific count:Now I see the real problem. The logic is:
- Button disabled →
generating - Button enabled →
enabled isDone = isEnabled- Poll fires when
isEnabledis true for 4 polls
But the arrow button (before the prompt is sent, on article 1) is also enabled — so everSawStreaming must go true before we can use isEnabled as a done signal. The everSawStreaming fallback forces it true after 40 polls (2 mins). But here’s the bug: after forcing everSawStreaming = true, if the button is the arrow (enabled) at that moment, it immediately starts the stability count and fires after just 4 more polls — even though Claude is still writing.
The real fix is to stop relying on button state at all and instead use data-is-streaming directly, which is what Claude actually sets. Let me also add a data-is-streaming="false" check since that’s the definitive signal:The timeout error revealed the real bug — the poll loop was counting 100 polls (5 minutes) without ever detecting completion, which means isDone was never becoming true.
The old detection relied on button disabled/enabled state combined with everSawStreaming. The problem: after 40 polls the code forced everSawStreaming = true, and since the arrow button (pre-send) is also enabled, the stability counter started immediately. But 100 polls later it still hadn’t triggered — meaning isEnabled was never consistently true either, probably because getButtonState() kept returning 'unknown'.
The new approach drops all button detection entirely and uses data-is-streaming="false" directly — the attribute Claude itself sets on the message element when generation completes. This is the ground truth signal, completely independent of button state, SVG icons, or any visual element. It either exists or it doesn’t.