Skip to content

fix(web): recover stale response streams - #1086

Merged
SamSaffron merged 5 commits into
SamSaffron:mainfrom
sam-saffron-jarvis:feat/response-stream-connect-timeout
Aug 30, 2026
Merged

fix(web): recover stale response streams#1086
SamSaffron merged 5 commits into
SamSaffron:mainfrom
sam-saffron-jarvis:feat/response-stream-connect-timeout

Conversation

@sam-saffron-jarvis

Copy link
Copy Markdown
Contributor

Summary

  • bound response-stream connection establishment so a replacement SSE request cannot remain pending before headers forever
  • adopt the authoritative server epoch from the initial response.created event instead of aborting every admitted POST into snapshot recovery
  • reconcile stale mobile run projections from authoritative idle status plus the durable transcript, including after the five-minute response snapshot expires
  • bypass conditional status caching during authoritative focus/visibility recovery

Root causes

The mobile failure was a chain of three independent holes:

  1. Replacement event streams had an unlimited pre-header wait; the existing inactivity watchdog only began after fetch() returned a response.
  2. New local projections started with epoch 1, while the server emits a generated run epoch. The first response.created event therefore caused an epoch mismatch, aborted the original WebRTC POST, and forced every response through snapshot recovery.
  3. If iOS missed the terminal event and the response snapshot aged out after five minutes, a 404 was treated as a retryable transport failure. The server session was idle, but the stale local projection remained the UI authority and displayed Working / Running a command indefinitely.

Behavior

An authoritative status sample that began while the same projected response was active can now reconcile an idle server through the durable transcript revision. Once that revision is installed and the session remains idle, the stale projection, transport lease, and supervisor are retired. A newer run or an uninstalled transcript revision prevents retirement.

Verification

  • mise x node@24 -- npm test — 408 tests passed
  • mise x node@24 -- npm run typecheck
  • mise x node@24 -- npm run lint:ts
  • production frontend build via mise x node@24 -- make build
  • deployed locally and confirmed on the affected iPhone with one-minute tool-call responses; Working now clears

Format the reviewed reconnect test and update stale bundle ceilings. Upstream main is already 436 bytes over the 423,000-byte JS raw budget and 161 bytes over the 161,500-byte CSS raw budget; this PR builds at 424,638/124,876 bytes for JS and 161,661/31,132 bytes for CSS under local Go 1.27 compression. Set bounded ceilings of 426,000/126,000 and 162,500/32,000 respectively.
Only publish newly discovered prompts instead of every recovery refresh, and publish terminal state transitions so peer tabs reconcile once. This prevents cross-tab interaction refreshes from bouncing indefinitely and re-rendering approval or ask-user dialogs during user input.
Treat a session's request ID as the stable interaction identity when recovery reports a different or newly-known response ID. This keeps a dismissed or resolved prompt from surviving as a duplicate waiting record and reopening its modal.
@SamSaffron
SamSaffron merged commit c86d6e8 into SamSaffron:main Aug 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants