Skip to content

fix(server): close expired dashboard selection streams quietly when no frames are queued - #4095

Open
lidge-jun wants to merge 1 commit into
devfrom
codex/pr4069-quiet-selection-stream-expiry
Open

fix(server): close expired dashboard selection streams quietly when no frames are queued#4095
lidge-jun wants to merge 1 commit into
devfrom
codex/pr4069-quiet-selection-stream-expiry

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #4069.

Since v2.47.0, an expired dashboard session left the account-selection event stream erroring with a NotAllowedError DOMException on its next send — including the 15-second heartbeat — and Bun prints every errored response stream to the server console, so the console filled with repeated DOMException dumps.

controller.error() is not incidental: it is the mechanism that discards frames already queued before revocation, so a revoked consumer cannot drain them. That contract is preserved. The stream now discriminates on queue state: with a non-empty queue it still errors and discards; with an empty queue — the common expired-session path, including every post-expiry heartbeat — it closes quietly, pending reads resolve done, and there is no error for Bun to dump.

The dashboard consumer treats a clean EOF and a stream error identically (same retry-with-backoff path, then quiet 401 on re-admission), so reconnect behavior is unchanged. Admission 401 and per-frame revalidation are unchanged.

The queue watermark is a named constant shared by the stream options and the check, so a future watermark change cannot mis-classify a non-empty queue.

Security review: this touches the management-session revocation path (src/server/management/). The revocation discard contract is preserved and now directly test-covered; per MAINTAINERS.md this still warrants explicit security review before merge.

Verification

  • Head: 589daec, based on dev 8026405.
  • Remote GitHub Actions on the exact head: 25 checks pass, 0 fail, 2 dispatch-only skips: main run https://github.com/lidge-jun/opencodex/actions/runs/34323955157 (Linux shards, macOS, gates incl. typecheck + privacy scan, keyring, npm-global, storage policy, api usage, docker smoke); enforce-target/hygiene/label all pass.
  • Regression coverage (runs in CI): the logout/expiry and heartbeat liveness tests now assert the quiet close; a new negative test queues a frame before revocation and proves it is discarded (reader rejects, payload never delivered).
  • Independent read-only design audit (Grok): queue-empty ⟺ desiredSize equals the watermark (verified including the pending-read case); both branches preserve no-drain-after-revocation; only the two updated tests depended on the old rejection; the dashboard treats EOF and error the same. Verdict: GO-WITH-FIXES, one blocker folded (named watermark constant).
  • Local suite, typecheck, and privacy scan: NOT RUN (local execution restricted in the preparing environment); the remote jobs provide this evidence.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

…o frames are queued

The account-selection SSE stream errored with a DOMException on every
post-revocation send, including the 15s heartbeat of an expired dashboard
session, and Bun prints each errored response stream to the server console.

error() exists to discard frames queued before revocation so a revoked
consumer cannot drain them; that contract is preserved for a non-empty
queue. When the queue is empty (the common expiry path) the stream now
closes quietly: pending reads resolve done and there is no error to dump.

The two liveness tests flip to the quiet-close expectation, and a new
negative test proves queued frames are still discarded on revocation.

Closes #4069
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 9, 2026 07:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T07:31:31.320407Z 589daec PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 3775c281-683f-4321-985e-8cd281a3376a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b379b9 and 589daec.

📒 Files selected for processing (2)
  • src/server/management/account-selection-stream.ts
  • tests/oauth/oauth-accounts-api.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The account-selection stream now closes quietly after session expiry when no frame is queued. It reports NotAllowedError only when revocation discards a queued frame. Tests cover both outcomes and clean up heartbeat resources.

Changes

Account stream revocation handling

Layer / File(s) Summary
Conditional stream closure
src/server/management/account-selection-stream.ts:6, src/server/management/account-selection-stream.ts:40-50, src/server/management/account-selection-stream.ts:73
The stream defines STREAM_QUEUE_HIGH_WATER_MARK = 16. When the revoked consumer has queued data, it reports NotAllowedError and closes. When the queue is empty, it closes without emitting the expected DOMException.
Revocation outcome tests
tests/oauth/oauth-accounts-api.test.ts:159-161, tests/oauth/oauth-accounts-api.test.ts:184-208
Tests expect quiet completion for expired sessions with no queued frames. A new test verifies that queued frames are discarded and the next read rejects with NotAllowedError. Heartbeat test cleanup now cancels the reader and restores the interval mock.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 589da

Expired dashboard account-selection streams now close quietly when empty, while queued updates are still discarded after revocation. The covered behavior is ready to merge.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue [#4069]. Empty revoked-stream queues now close quietly, pending reads resolve with done: true, and repeated NotAllowedError console output is avoided. Queued frames still tri…
Out of Scope Changes check ✅ Passed The changes are limited to account-selection stream behavior and its tests. They directly support [#4069]. No unrelated code or behavior changes are identified.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: expired dashboard selection streams now close quietly when no frames are queued.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/pr4069-quiet-selection-stream-expiry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lidge-jun lidge-jun added the bug Something isn't working label Sep 9, 2026
@lidge-jun

Copy link
Copy Markdown
Owner Author

@/home/box/developer/review-comments/pr-4095.md

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 65 / 80

이 PR은 대시보드 관리 세션이 만료된 뒤에도 account-selection SSE가 계속 controller.error(NotAllowedError)를 내서 Bun 서버 콘솔에 DOMException이 반복 쌓이던 #4069를 고칩니다. 지금 dev HEAD 4b379b9ec(package 2.49.0, tip #3914 OrcaRouter sponsor)의 src/server/management/account-selection-stream.ts는 권한 검사 실패 시 무조건 error()로 끊습니다. error()는 취소 전에 큐에 남은 프레임을 버려서 폐기된 소비자가 이벤트를 읽지 못하게 하는 보안 계약이라 필요합니다. 다만 만료 후 15초 heartbeat처럼 큐가 비어 있는 흔한 경로에서는 버릴 프레임이 없는데도 예외가 찍힙니다. 이번 변경은 STREAM_QUEUE_HIGH_WATER_MARK = 16을 스트림 옵션과 같은 이름으로 공유하고, desiredSize < watermark일 때만 error·discard, 비어 있으면 close()만 호출합니다. 대시보드 쪽은 EOF와 stream error를 같은 재연결 경로로 처리한다고 본문에 적혀 있어 재접속 동작은 그대로입니다. types/config 분할과 무관하고, 슬라이스 백로그(#3719 등)와도 겹치지 않는 운영 소음 수정입니다.

라인 src/server/management/account-selection-stream.ts · STREAM_QUEUE_HIGH_WATER_MARK - highWaterMark와 판정 상수를 하나로 묶었습니다. 나중에 숫자만 바꾸면 빈 큐를 오판하던 위험을 막습니다.
라인 account-selection-stream.ts · desiredSize < watermark - 큐가 비면 quiet close, 프레임이 있으면 기존처럼 NotAllowedError로 discard합니다. 의도와 ReadableStream 계약이 맞습니다.
라인 tests/oauth/oauth-accounts-api.test.ts - logout/expiry·heartbeat는 done으로 바뀌고, 큐에 프레임을 넣은 뒤 revoke하는 새 negative 테스트가 discard를 잠급니다. 회귀 범위가 충분합니다.
경로 보안 - management 세션 폐기 경로라 MAINTAINERS.md상 명시 보안 리뷰가 맞습니다. 계약(폐기 후 drain 금지)은 테스트로 잠겼으니 리뷰 체크만 하면 됩니다.
경로 base - 작성 기준이 8026405d9이고 지금 tip은 #3914입니다. 파일 충돌 가능성은 낮지만 rebase/CI를 tip에 맞춰 한 번 확인하세요.

메인테이너의 판단이 필요한 지점

  • management SSE quiet-close를 2.49 핫픽스에 바로 넣을지
  • 보안 리뷰를 이 PR만 별도 통과시킬지, 같은 주 management 변경과 묶을지
  • Bun 콘솔 덤프가 프로덕션에서 실제로 얼마나 아픈지(우선순위 확인)

너의 추천
exact-head CI가 그린 뒤 보안 리뷰 한 줄 확인하고 머지하세요. #4069를 닫는 작은 운영 수정이고 discard 계약 테스트가 있어 리스크가 낮습니다. #4094/#4086과 경로가 달라 병렬 랜딩해도 됩니다.

이 댓글은 grok-bot이 작성했습니다

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 589daec against its merge base 8026405. Approving the scoped stream-close fix.

The distinction is correct for this default count-based ReadableStream strategy: desiredSize equals the shared watermark when no chunks are queued. A revoked empty stream closes without emitting another frame; a revoked non-empty stream still errors before cleanup, which discards the queued payload. The new queued-frame negative control checks reader rejection, while pending-read/heartbeat controls check done=true. Admission and the per-send authorization callback are unchanged, and cleanup still removes the subscription, interval, abort listener and connection entry.

I traced the dashboard reader: clean EOF and read errors converge on the same finally/backoff path, so the quiet close does not remove reconnect behavior. This is not a redesign or permission to skip the revocation check.

Exact-head repository CI 34323955157 completed successfully: 19 successful jobs, 2 skipped jobs (macos control and the Windows shard matrix). Gates and Linux/macOS test shards passed. The skipped Windows matrix is not being represented as Windows full-suite evidence. No local product test or live session manipulation was performed.

Scoped security review: the queued-data discard rule on observed revocation is retained; the patch only suppresses an expected error when there is no queued payload to discard. This is a human-reviewed merge candidate, subject to final repository checks. No merge or deployment was performed by this review.

lidge-jun added a commit that referenced this pull request Sep 9, 2026
lidge-jun added a commit that referenced this pull request Sep 9, 2026
lidge-jun added a commit that referenced this pull request Sep 9, 2026
lidge-jun added a commit that referenced this pull request Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants