Skip to content

fix(ui): enforce owner-routed event streams - #81

Merged
delete-cloud merged 9 commits into
mainfrom
phase2-events-sticky-routing
Apr 27, 2026
Merged

delete-cloud merged 9 commits into
mainfrom
phase2-events-sticky-routing

Conversation

@delete-cloud

@delete-cloud delete-cloud commented Apr 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Add a Phase 2 ADR for owner-routed HTTP event streams and sticky routing.
  • Enforce owner checks on GET /sessions/{session_id}/events before stream registration and during keepalive revalidation.
  • Add failover coverage that proves stale owners are rejected and current owners keep streaming.

Verification

  • uv run pytest tests/ui/test_http_server_failover.py tests/ui/test_http_server.py::TestEventsFanOut::test_event_queues_registered tests/ui/test_http_server.py::TestEventsFanOut::test_multiple_queues_in_session tests/ui/test_http_server.py::TestEventsFanOut::test_event_queue_cleanup_is_shielded_on_disconnect tests/ui/test_http_server.py::TestEventsFanOut::test_event_queue_cleanup_ignores_missing_session tests/ui/test_http_server.py::TestEventsFanOut::test_event_generator_uses_public_session_apis_for_keepalive_exit tests/ui/test_http_server.py::TestEventsFanOut::test_event_generator_exits_cleanly_when_session_disappears_during_keepalive tests/ui/test_session_manager_owner_checks.py::test_run_agent_rejects_non_owner_instance tests/ui/test_session_manager_owner_checks.py::test_restore_checkpoint_rejects_stale_owner tests/ui/test_session_manager_owner_checks.py::test_close_session_rejects_stale_owner tests/ui/test_session_manager_owner_checks.py::test_ensure_session_runtime_rejects_stale_owner tests/ui/test_session_manager_owner_checks.py::test_submit_approval_rejects_stale_owner -v

Summary by CodeRabbit

  • New Features

    • Session event streams now enforce owner-routed authorization; non-owner or stale-owner instances are rejected with HTTP 409 before stream registration
    • Streaming connections re-check ownership during keepalive/timeout cycles and terminate if ownership is lost
    • Missing sessions return HTTP 404 during event access/registration
    • Streams emit an initial SSE "ping" event when established
  • Documentation

    • Added ADR documenting Phase 2 owner-routed policy for SSE session event streams
  • Tests

    • Added failover test suite covering ownership, termination, delivery, and error cases

delete-cloud and others added 2 commits April 21, 2026 13:12
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Copilot AI review requested due to automatic review settings April 21, 2026 05:15
@coderabbitai

coderabbitai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Enforces Phase 2 owner-routed policy for HTTP SSE session event streams: session lookup and authorization occur before event-queue registration, ownership is re-verified on keepalive and per-event, ownership loss returns/terminates with 409 Conflict, SessionManager exposes public authorize/verify/register APIs, and tests covering failover and staleness were added.

Changes

Cohort / File(s) Summary
ADR
coding-agent/docs/adr/0015-enforce-owner-routed-http-event-streams.md
New ADR defining Phase 2 owner-routed policy for /sessions/{session_id}/events: authorize-before-register, re-check ownership during keepalive/per-event, 409 Conflict semantics, sticky-routing restriction, and acceptance criteria.
Session Manager API
coding-agent/src/coding_agent/ui/session_manager.py
Added public async methods authorize_event_stream(session_id), verify_event_stream_ownership(session_id), and register_owned_event_queue_async(session_id, queue) that delegate to owner assertions and ensure queue cleanup on ownership loss; refactored session removal locking.
HTTP Server SSE endpoint
coding-agent/src/coding_agent/ui/http_server.py
GET /sessions/{session_id}/events now uses get_session_async(...) (KeyError→404), calls authorize_event_stream() before creating/registering queue, uses register_owned_event_queue_async() (maps SessionOwnershipConflictError→409), and re-checks ownership via verify_event_stream_ownership() before yielding events and after keepalive timeouts, terminating stream on ownership loss.
Failover & tests
coding-agent/tests/ui/test_http_server_failover.py, coding-agent/tests/ui/test_http_server.py
Added comprehensive failover tests (test_http_server_failover.py) covering missing-session 404, pre-registration 409, mid-stream ownership changes (stream termination and dropped events), keepalive re-checks with FakeOwnerStore; adjusted keepalive-related expectation in test_http_server.py.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant HTTPServer
    participant SessionMgr
    participant OwnerStore
    participant StreamGen

    Client->>HTTPServer: GET /sessions/{id}/events
    HTTPServer->>SessionMgr: get_session_async(session_id)
    alt session missing
        SessionMgr-->>HTTPServer: raise KeyError (mapped to 404)
        HTTPServer-->>Client: 404 Session not found
    else session exists
        HTTPServer->>SessionMgr: authorize_event_stream(session_id)
        SessionMgr->>OwnerStore: get_owner / validate fencing token
        OwnerStore-->>SessionMgr: owner info or stale
        SessionMgr-->>HTTPServer: ok or raise SessionOwnershipConflictError (409)

        alt Owner Valid
            HTTPServer->>SessionMgr: register_owned_event_queue_async(session_id, queue)
            SessionMgr-->>HTTPServer: queue registered or raise SessionOwnershipConflictError (409)
            HTTPServer-->>Client: 200 SSE stream established
            loop keepalive / per-event
                StreamGen->>SessionMgr: verify_event_stream_ownership(session_id)
                SessionMgr->>OwnerStore: get_owner / validate
                OwnerStore-->>SessionMgr: owner valid or changed
                alt still owner
                    StreamGen-->>Client: event or ping
                else owner changed/stale
                    SessionMgr-->>StreamGen: raise SessionOwnershipConflictError
                    StreamGen->>StreamGen: terminate generator and cleanup queue
                    StreamGen-->>Client: stream closed
                end
            end
        else Owner Invalid/Stale
            HTTPServer-->>Client: 409 Conflict (SessionOwnershipConflictError)
        end
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

  • feat: add phase 2 owner substrate #79: Introduced owner-fencing substrate (_assert_owner, SessionOwnershipConflictError, owner-store primitives) that the new SessionManager public methods and SSE ownership checks rely on.
  • feat(ui): complete postgres phase1 persistence #69: Added/modified async SessionManager and SSE endpoint patterns (get_events / add_event_queue_async → register-owned flow) that this PR extends to enforce owner-routed behavior.

Poem

🐰 I hop where streams and fencing meet,
I check the token before I greet.
A ping, a pause — I watch the gate,
If owner slips, I close the state.
Queues cleared tidy — routed neat.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(ui): enforce owner-routed event streams' accurately describes the main change: adding owner-routed enforcement to HTTP event streams with up-front checks and keepalive revalidation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase2-events-sticky-routing

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens Phase 2 multi-instance semantics for the UI’s SSE event stream by enforcing session ownership (“owner-routed” behavior) on GET /sessions/{session_id}/events, and documents the decision in a new ADR.

Changes:

  • Add SessionManager.authorize_event_stream() and SessionManager.verify_event_stream_ownership() as public wrappers around the existing owner fencing logic.
  • Enforce ownership before SSE queue registration and re-validate ownership on keepalive timeouts to terminate stale streams.
  • Add failover-focused SSE tests and an ADR describing the Phase 2 sticky-routing policy for event streams.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
coding-agent/src/coding_agent/ui/http_server.py Adds owner checks to the SSE /events handler before stream registration and during keepalive revalidation.
coding-agent/src/coding_agent/ui/session_manager.py Introduces public event-stream ownership authorization/revalidation methods built on existing _assert_owner.
coding-agent/tests/ui/test_http_server_failover.py Adds coverage ensuring missing sessions 404 before owner checks, stale owners are rejected, and streams stop when ownership changes.
coding-agent/docs/adr/0015-enforce-owner-routed-http-event-streams.md Documents the Phase 2 decision/acceptance criteria for owner-routed SSE with sticky routing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +15 to +18
from coding_agent.ui.session_owner_store import (
SessionOwnerRecord,
SessionOwnershipConflictError,
)

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

SessionOwnershipConflictError is imported but never used in this test module. Please remove the unused import to keep the test file clean (and avoid failing future linting if introduced).

Suggested change
from coding_agent.ui.session_owner_store import (
SessionOwnerRecord,
SessionOwnershipConflictError,
)
from coding_agent.ui.session_owner_store import SessionOwnerRecord

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
coding-agent/src/coding_agent/ui/http_server.py (1)

667-682: ⚠️ Potential issue | 🟠 Major

Re-check ownership before yielding queued events, not only on keepalive.

This only fences the stream after a 30s idle timeout. If the queue keeps receiving events, the timeout path never runs, so a stale instance can continue serving an already-open SSE stream after ownership has moved. The same gap exists if ownership flips after the preflight check but before queue registration.

Suggested fix
                 try:
                     event = await asyncio.wait_for(queue.get(), timeout=30.0)
+                    try:
+                        await session_manager.verify_event_stream_ownership(session_id)
+                    except SessionOwnershipConflictError:
+                        break
                     yield event
                     if event.get("event") == "SessionClosed":
                         break
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coding-agent/src/coding_agent/ui/http_server.py` around lines 667 - 682,
event_generator currently only verifies ownership on timeout, so if events keep
arriving a stale instance can yield them; modify event_generator to re-check
session ownership immediately after awaiting queue.get() and before yielding
each event by calling session_manager.verify_event_stream_ownership(session_id)
(and/or session_manager.has_session_async(session_id) as appropriate) and handle
SessionOwnershipConflictError by breaking the loop (and not yielding the event);
ensure the SessionClosed behavior still works and preserve the existing
timeout-based checks.
🧹 Nitpick comments (1)
coding-agent/tests/ui/test_http_server_failover.py (1)

78-94: Drop the dead cleanup loops or stop swallowing teardown failures.

session_manager.clear_sessions() already empties the store before each list_sessions() call here, so these loops are currently no-ops. Keeping them behind except Exception: pass means a future cleanup regression will be silently masked.

Suggested simplification
 `@pytest.fixture`(autouse=True)
 async def clear_sessions():
     session_manager.clear_sessions()
     limiter.reset()
-    for session_id in list(session_manager.list_sessions()):
-        try:
-            await session_manager.close_session(session_id)
-        except Exception:
-            pass
     yield
     session_manager.clear_sessions()
-    for session_id in list(session_manager.list_sessions()):
-        try:
-            await session_manager.close_session(session_id)
-        except Exception:
-            pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coding-agent/tests/ui/test_http_server_failover.py` around lines 78 - 94, The
fixture clear_sessions has redundant loops that iterate
session_manager.list_sessions() immediately after calling
session_manager.clear_sessions() and also swallows all teardown errors; remove
the dead loops and/or stop masking failures: delete both for-blocks that call
session_manager.list_sessions() and await
session_manager.close_session(session_id), leaving
session_manager.clear_sessions() and limiter.reset() in setup and
session_manager.clear_sessions() in teardown, or if explicit close is required
keep the loops but replace the bare except Exception: pass with proper error
handling (log and re-raise or assert) so failures from
session_manager.close_session(...) are not silently ignored; refer to
clear_sessions, session_manager.clear_sessions(), limiter.reset(),
session_manager.list_sessions(), and session_manager.close_session() when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@coding-agent/src/coding_agent/ui/http_server.py`:
- Around line 667-682: event_generator currently only verifies ownership on
timeout, so if events keep arriving a stale instance can yield them; modify
event_generator to re-check session ownership immediately after awaiting
queue.get() and before yielding each event by calling
session_manager.verify_event_stream_ownership(session_id) (and/or
session_manager.has_session_async(session_id) as appropriate) and handle
SessionOwnershipConflictError by breaking the loop (and not yielding the event);
ensure the SessionClosed behavior still works and preserve the existing
timeout-based checks.

---

Nitpick comments:
In `@coding-agent/tests/ui/test_http_server_failover.py`:
- Around line 78-94: The fixture clear_sessions has redundant loops that iterate
session_manager.list_sessions() immediately after calling
session_manager.clear_sessions() and also swallows all teardown errors; remove
the dead loops and/or stop masking failures: delete both for-blocks that call
session_manager.list_sessions() and await
session_manager.close_session(session_id), leaving
session_manager.clear_sessions() and limiter.reset() in setup and
session_manager.clear_sessions() in teardown, or if explicit close is required
keep the loops but replace the bare except Exception: pass with proper error
handling (log and re-raise or assert) so failures from
session_manager.close_session(...) are not silently ignored; refer to
clear_sessions, session_manager.clear_sessions(), limiter.reset(),
session_manager.list_sessions(), and session_manager.close_session() when making
the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 94a4d923-308e-412f-b17e-7ea7dd6111d9

📥 Commits

Reviewing files that changed from the base of the PR and between 3f991fd and 01e0a5a.

📒 Files selected for processing (4)
  • coding-agent/docs/adr/0015-enforce-owner-routed-http-event-streams.md
  • coding-agent/src/coding_agent/ui/http_server.py
  • coding-agent/src/coding_agent/ui/session_manager.py
  • coding-agent/tests/ui/test_http_server_failover.py

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 655 to 665
if not await session_manager.has_session_async(session_id):
raise HTTPException(status_code=404, detail="Session not found")

try:
await session_manager.authorize_event_stream(session_id)
except SessionOwnershipConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc

session = await session_manager.get_session_async(session_id)
queue: asyncio.Queue[dict[str, str]] = asyncio.Queue(maxsize=100)
await session_manager.add_event_queue_async(session_id, queue)

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

get_events() does an existence check via has_session_async() and then calls get_session_async(). For uncached sessions this results in two store loads and still leaves a TOCTOU window where the session can disappear between calls, causing an uncaught KeyError (500) from get_session_async(). Consider using a single get_session_async() in a try/except KeyError to return 404, then run authorize_event_stream() afterward (and proceed to queue registration).

Suggested change
if not await session_manager.has_session_async(session_id):
raise HTTPException(status_code=404, detail="Session not found")
try:
await session_manager.authorize_event_stream(session_id)
except SessionOwnershipConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
session = await session_manager.get_session_async(session_id)
queue: asyncio.Queue[dict[str, str]] = asyncio.Queue(maxsize=100)
await session_manager.add_event_queue_async(session_id, queue)
try:
await session_manager.get_session_async(session_id)
except KeyError as exc:
raise HTTPException(status_code=404, detail=_key_error_detail(exc)) from exc
try:
await session_manager.authorize_event_stream(session_id)
except SessionOwnershipConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
queue: asyncio.Queue[dict[str, str]] = asyncio.Queue(maxsize=100)
try:
await session_manager.add_event_queue_async(session_id, queue)
except KeyError as exc:
raise HTTPException(status_code=404, detail=_key_error_detail(exc)) from exc

Copilot uses AI. Check for mistakes.
Comment on lines 672 to +686
event = await asyncio.wait_for(queue.get(), timeout=30.0)
try:
await session_manager.verify_event_stream_ownership(session_id)
except SessionOwnershipConflictError:
break
yield event
if event.get("event") == "SessionClosed":
break
except asyncio.TimeoutError:
if not await session_manager.has_session_async(session_id):
break
try:
await session_manager.verify_event_stream_ownership(session_id)
except SessionOwnershipConflictError:
break

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

verify_event_stream_ownership() is called for every queued event (and again on each keepalive timeout). In PG-backed deployments this likely means a database lookup per SSE message via SessionOwnerStore.get_owner(), which can become a throughput bottleneck for active streams. Consider verifying ownership only on keepalive timeouts (or with a time-based debounce / cached-lease approach) to bound the verification IO rate while still terminating streams promptly on owner loss.

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +313 to +315
if timeout_count == 1:
raise asyncio.TimeoutError
raise StopAsyncIteration

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

fake_wait_for raises StopAsyncIteration to try to terminate the SSE generator. Raising StopAsyncIteration from inside an awaited call in an async generator can be converted into a RuntimeError ("async generator raised StopAsyncIteration") if that code path is ever exercised, making the test brittle. Prefer driving generator termination via the existing control paths (e.g., raise asyncio.TimeoutError and have has_session_async return False / or force an ownership conflict) rather than raising StopAsyncIteration from the mocked wait_for.

Suggested change
if timeout_count == 1:
raise asyncio.TimeoutError
raise StopAsyncIteration
if timeout_count == 2:
owner_store._owners[session_id] = SessionOwnerRecord(
owner_id="owner-b",
lease_expires_at=datetime.now(UTC) + timedelta(seconds=30),
fencing_token=2,
)
raise asyncio.TimeoutError

Copilot uses AI. Check for mistakes.
await session_manager.add_event_queue_async(session_id, queue)
except KeyError as exc:
raise HTTPException(status_code=404, detail=_key_error_detail(exc)) from exc

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

There is a small race window between authorize_event_stream() and add_event_queue_async(): ownership can change after the authorization check but before the queue is registered, allowing a non-owner instance to register an in-memory queue (even if it will be dropped on the next keepalive/ownership check). If the intent is to guarantee “reject before stream registration”, consider coupling these operations (e.g., a single SessionManager method that asserts ownership and registers the queue under a shared lock / critical section, or re-check ownership immediately after registration and remove the queue on failure).

Suggested change
try:
await session_manager.verify_event_stream_ownership(session_id)
except SessionOwnershipConflictError as exc:
await _cleanup_event_queue_on_disconnect(session_id, queue)
raise HTTPException(status_code=409, detail=str(exc)) from exc

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@coding-agent/src/coding_agent/ui/session_manager.py`:
- Around line 580-593: register_owned_event_queue_async mutates
session.event_queues without using the same session-level lock used by
close_session(), allowing a race where close_session() deletes the session after
get_session_async() returns; fix by acquiring the same lock (self._lock) around
the check-and-append sequence: inside an "async with self._lock" block re-verify
ownership (call _assert_owner or check session presence/owner), fetch or
validate the session (get_session_async or direct lookup), append the queue to
session.event_queues, and only release the lock after append; keep the existing
except SessionOwnershipConflictError cleanup logic (remove queue if appended)
but ensure the append and removal are performed under the lock so a concurrent
close cannot leave a dangling registration; reference functions/members:
register_owned_event_queue_async, _assert_owner, get_session_async,
close_session, self._lock, session.event_queues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 418ec431-2704-48f5-b2bb-bfeddc5717cb

📥 Commits

Reviewing files that changed from the base of the PR and between 58a9553 and 18ab2e9.

📒 Files selected for processing (3)
  • coding-agent/src/coding_agent/ui/http_server.py
  • coding-agent/src/coding_agent/ui/session_manager.py
  • coding-agent/tests/ui/test_http_server_failover.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • coding-agent/tests/ui/test_http_server_failover.py

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

session.event_queues.append(queue)
try:
await self._assert_owner(session_id)
except SessionOwnershipConflictError:

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

register_owned_event_queue_async() only removes the newly-appended queue when _assert_owner() raises SessionOwnershipConflictError. If _assert_owner() (or the owner backend) raises any other exception after the append (e.g., backend/codec errors), the queue will remain registered on the session, which can leak memory and cause future broadcasts to enqueue into an orphaned queue. Consider ensuring the queue is removed on any exception raised after session.event_queues.append(queue) (not just ownership conflicts), and then re-raise the original error.

Suggested change
except SessionOwnershipConflictError:
except BaseException:

Copilot uses AI. Check for mistakes.
session_manager.clear_sessions()
limiter.reset()
yield
session_manager.clear_sessions()

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

The clear_sessions autouse fixture resets the global limiter before each test, but not in teardown. Because limiter is a module-level singleton shared across the whole test suite, leftover rate-limit state from the final test in this module can leak into subsequent tests and cause order-dependent 429s. Consider also calling limiter.reset() after yield (matching the pattern in tests/ui/test_http_server.py).

Suggested change
session_manager.clear_sessions()
session_manager.clear_sessions()
limiter.reset()

Copilot uses AI. Check for mistakes.
delete-cloud and others added 2 commits April 21, 2026 23:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@delete-cloud
delete-cloud merged commit 9e073a2 into main Apr 27, 2026
5 checks passed
@delete-cloud
delete-cloud deleted the phase2-events-sticky-routing branch May 2, 2026 13:16
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