What I measured
On a live https://chatgpt.com/ load, roughly 2 seconds after #saypi-callButton appears:
#saypi-callButton → present, exactly 1
#saypi-callButton.onclick → null ← the click handler property is gone
document.documentElement.dataset.saypiBuild → null ← our build stamp attribute is gone
document.body.className → "unlocked" ← the "chatgpt" flag class is gone
For contrast, the same probe on pi.ai returns a build stamp, body class="pi unlocked", and a live onclick.
updateCallButton sets callButton.onclick = onClick (src/buttons/CallButton.ts), and ChatbotService.addChatbotFlags() adds the chatgpt class to <body>, and stampBuildOnDocument() sets <html data-saypi-build>. All three are gone a couple of seconds later — attributes and JS properties — which points at ChatGPT's React/Next hydration replacing or resetting those nodes rather than at three separate bugs. A React hydration error (Minified React error #418) is logged on the same page.
Clicking the button immediately after it appears does work (measured: label → "End voice mode", callState → active), which is presumably why the host sweep passes — it drives its turn promptly.
Why it matters
If the button's onclick is genuinely dropped once hydration settles, a ChatGPT user who lands on the page and clicks a few seconds later gets a button that looks right and does nothing. That would be worse than a missing button, because there is no visible signal anything is wrong.
Two things are unproven and should be established first:
- Is the button actually dead after hydration, or only the
onclick property while a listener elsewhere still works? Click it ~5s and ~30s after load and observe callState.
- Does
CallButton.this.element still point at the node in the DOM? If React cloned the node, SayPi holds a detached element and every subsequent state update (icon, segments, glow, enable/disable) drives a node nobody can see.
Relationship to #593
Distinct failure mode. #593 (fixed in #594) is element absent, which ensureCallButtonPresent() detects and repairs. This one is element present but inert — a presence check can't see it, so #594 does not address it and shouldn't be assumed to.
If confirmed, the likely remedy is to stop relying on JS properties surviving on host-owned subtrees: bind with addEventListener, verify CallButton.this.element.isConnected on each state update, and re-adopt (or re-create) the button when the node in the DOM is not the one we hold.
Not a regression
Measured identically on main (a52dd18) and on the #594 branch, so this predates both. Found while doing cross-host verification for #593.
Repro: Layer 4 (CDP), real Chrome, dev build loaded, https://chatgpt.com/, wait for #saypi-callButton, then after ~2s evaluate document.getElementById("saypi-callButton").onclick.
What I measured
On a live
https://chatgpt.com/load, roughly 2 seconds after#saypi-callButtonappears:For contrast, the same probe on pi.ai returns a build stamp,
body class="pi unlocked", and a liveonclick.updateCallButtonsetscallButton.onclick = onClick(src/buttons/CallButton.ts), andChatbotService.addChatbotFlags()adds thechatgptclass to<body>, andstampBuildOnDocument()sets<html data-saypi-build>. All three are gone a couple of seconds later — attributes and JS properties — which points at ChatGPT's React/Next hydration replacing or resetting those nodes rather than at three separate bugs. A React hydration error (Minified React error #418) is logged on the same page.Clicking the button immediately after it appears does work (measured: label → "End voice mode",
callState→active), which is presumably why the host sweep passes — it drives its turn promptly.Why it matters
If the button's
onclickis genuinely dropped once hydration settles, a ChatGPT user who lands on the page and clicks a few seconds later gets a button that looks right and does nothing. That would be worse than a missing button, because there is no visible signal anything is wrong.Two things are unproven and should be established first:
onclickproperty while a listener elsewhere still works? Click it ~5s and ~30s after load and observecallState.CallButton.this.elementstill point at the node in the DOM? If React cloned the node, SayPi holds a detached element and every subsequent state update (icon, segments, glow, enable/disable) drives a node nobody can see.Relationship to #593
Distinct failure mode. #593 (fixed in #594) is element absent, which
ensureCallButtonPresent()detects and repairs. This one is element present but inert — a presence check can't see it, so #594 does not address it and shouldn't be assumed to.If confirmed, the likely remedy is to stop relying on JS properties surviving on host-owned subtrees: bind with
addEventListener, verifyCallButton.this.element.isConnectedon each state update, and re-adopt (or re-create) the button when the node in the DOM is not the one we hold.Not a regression
Measured identically on
main(a52dd18) and on the #594 branch, so this predates both. Found while doing cross-host verification for #593.Repro: Layer 4 (CDP), real Chrome, dev build loaded,
https://chatgpt.com/, wait for#saypi-callButton, then after ~2s evaluatedocument.getElementById("saypi-callButton").onclick.