Fix attachment uploads: accept-aware input selection, upload-settle wait, scoped stop detection, reply-baseline wait - #55
Open
adossey wants to merge 4 commits into
Conversation
…ait, scoped stop detection, reply-baseline wait Four compounding bugs made attachments hang at 0% and queries fail or return empty text: 1. setFileInputFiles picked file inputs in reverse DOM order, which on chatgpt.com selects #upload-camera (accept="image/*"). Documents fed to an image-only input enter the site's image pipeline, whose decode rejects uncaught — tile stuck at 0%, no network request ever made. Both backends now read each input's accept attribute and prefer accept-compatible inputs (image-only inputs are used only when every file is an image). 2. query() sent immediately after attaching; providers disable send while uploads are in flight, so every send path failed (send_not_triggered) while tiles were still uploading. New #waitForUploadsSettled polls until an enabled send button with no in-flight upload indicator holds stable, with a byte-scaled timeout, and surfaces attachment_upload_failed/timeout. 3. Stop-button detection matched sidebar history rows (aria-label*="cancel" vs a conversation titled "…Cancellations"), failing sends with already_generating. Stop lookups are now scoped out of nav/aside/history. Also: when a provider replaces send with stop while responding, "no send button" no longer counts as send-enabled, so generating is detected during long thinking phases. 4. waitForAssistantStable accepted pre-existing page text as the reply after 2.5s — reasoning models that think silently for minutes returned empty text. A reply now requires a NEW assistant message beyond the baseline count (with long-grace escape hatches for selector-less providers). Also: enable Target.setDiscoverTargets so Target.targetDestroyed actually fires — closed tabs were never pruned and later queries hung on dead sessions for their full timeout; waitForPromptVisible now surfaces tab_target_lost after repeated evaluate failures instead of silently spinning. Verified live against chatgpt.com: attach -> upload (backend-api/files -> CDN PUT -> process_upload_stream) -> send -> wait through "Pro thinking" -> correct reply text returned. Test suite: 163/163 pass.
The send-confirmation poll used the raw stop selector, so out-of-scope matches (e.g. sidebar history rows matching aria-label*="cancel") made it report "sent" instantly even when the click never landed — the query then waited on a reply that was never requested and returned page chrome as text. Same scoping as the other three stop-detection sites.
…min response cap, auth-only liveness Three layers silently capped long queries far below what reasoning models need (they can think for an hour before responding): - requestJson used Node's built-in fetch, whose undici transport enforces a ~5-minute headers timeout — long-blocking /query calls died with 'fetch failed' at exactly 300s. Default transport is now node:http with an optional client timeout; a custom fetchImpl (tests) keeps the fetch path. - /query clamped timeoutMs to 30 minutes; the ceiling is now 2 hours. - chatgpt-controller.query() capped the response wait at 8 minutes regardless of the requested timeout; it now honors the full budget. Also: validateConn treated any non-ok /status as 'not running', but /status carries tab-level errors (tab_not_found before a default tab exists) while the server is healthy — only auth failures invalidate a connection now.
Human-paced typing at 12-45ms/char meant a 29KB prompt took ~35 minutes to type — during which a stray send could fire with a PARTIAL message (the assistant then answers incomplete input) and the eventual clickSend fails with already_generating. Prompts over 1500 chars are now inserted in bulk via Input.insertText (paste-like, no key events), with a short human-typed tail; short prompts keep the existing pacing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Attachment uploads to chatgpt.com hung forever at 0% (file tile renders, spinner never moves, no network request), and long-thinking models returned empty text. Debugging against a live session via CDP surfaced five compounding bugs; this PR fixes all of them.
1. Documents were fed to the image-only file input (the 0%-spinner root cause)
setFileInputFilestried file inputs in reverse DOM order. chatgpt.com ships three inputs —#upload-files(the composer),#upload-photos(accept="image/*"),#upload-camera(accept="image/*") — so reverse order picks#upload-camera. A document dropped into an image-only input enters the site's image pipeline, whose decode rejects uncaught insideuploadFile(verified viaRuntime.exceptionThrown): the tile renders inUploadingstate and no upload request is ever issued. Both backends (Chrome CDP + Electron) now read each input'sacceptattribute and prefer accept-compatible inputs; image-only inputs are used only when every file is an image.2. Send raced in-flight uploads
query()clicked send immediately after attaching. Providers disable send while uploads are in flight, so every send strategy failed within seconds (send_not_triggered) while tiles were legitimately still uploading. New#waitForUploadsSettled()runs between typing and sending: polls for a visible enabled send button with no in-flight upload indicator in the composer, held stable across polls; byte-scaled timeout (2min base + 1min/MB, 15min cap); surfacesattachment_upload_failed/attachment_upload_timeoutwith data instead of a misleading send error. Emits the existinguploading_filesprogress phase.3. Stop-button detection matched sidebar history ("…Cancellations")
The stop selector includes
button[aria-label*="cancel" i], which matched sidebar history rows for a conversation titled "JetBlue West Coast Cancellations" — every send failed withalready_generating. Stop lookups (#clickSendguard,#clickVisibleStop,#waitForAssistantStable) are now scoped out ofnav/aside/[role=navigation]/historycontainers. Additionally, when a provider replaces the send button with the stop button while responding, the absence of a send button no longer counts as "send enabled" — so generating is correctly detected during long thinking phases.4. Empty replies from reasoning models
#waitForAssistantStableaccepted pre-existing page text as "the reply" after 2.5s of stability — models that think silently for minutes (e.g. "Pro thinking") returned success with empty/garbage text. Completion now requires a new assistant message beyond the count captured when waiting began, with escape hatches for selector-less providers (45s fallback grace) and a baseline-glitch guard (2min of stable, non-generating quiet).5. Closed tabs were never pruned (silent 10-minute hangs)
A
Target.targetDestroyedlistener was registered, butTarget.setDiscoverTargetswas never enabled — Chrome never emits those events without it. Closing a tab left a zombie record whose dead session made every evaluate throw;waitForPromptVisibleswallowed the errors and spun silently until full timeout. Discovery is now enabled, andwaitForPromptVisiblesurfacestab_target_lostafter repeated consecutive evaluate failures so callers can recreate the tab.Verification
POST /backend-api/files→ CDN PUT →process_upload_stream) → send → waits through an extended "Pro thinking" phase → returns the exact reply text. Previously: tile frozen at 0% andsend_not_triggered/already_generating/ empty-text results.npm test: 163/163 pass.change/inputevents fire with correct File+mime, zero upload fetches from the page, captured the uncaught rejection in the site'suploadFile, and bisected the three file inputs to find only#upload-filestriggers the real upload chain.