Skip to content

feat(os-events): /api/os/events SSE stream and useOsEvents hook (supersedes #2309) - #2378

Merged
jaylfc merged 4 commits into
devfrom
exec/tsk-ronapx
Aug 12, 2026
Merged

jaylfc merged 4 commits into
devfrom
exec/tsk-ronapx

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 12, 2026 •

Copy link
Copy Markdown
Owner

Card: tsk-ronapx. Supersedes #2309 (branch exec/tsk-jmctoa), which had been red and unattended since 2026-08-06 with no lane left to answer the review. STEP 0 of the card was done as instructed: git merge origin/exec/tsk-jmctoa onto current dev, which merged clean. Every item below is on top of that.

Close #2309 as superseded when this merges.

1 (BLOCKING) — the subscription and task leak

Confirmed exactly as carded. Both event_bus.subscribe() calls and both asyncio.create_task(_relay(...)) calls ran in the handler body, while teardown lived in gen()'s finally. StreamingResponse.body_iterator is an async generator, and an async generator closed without ever being iterated never runs its body — so that finally never runs. A client disconnecting between the handler returning and the stream starting leaked two subscriptions and two never-cancelled tasks per occurrence. EventBus.subscribe hands out an unbounded asyncio.Queue and _publish_to_channel does put_nowait into every registered queue, so the bus then copies every later event into queues nobody will ever drain.

Fix: setup moved inside gen(), immediately inside the try, so the finally can only ever undo setup that actually happened (the handles start as None / [] and the teardown is guarded, so a failure part-way through setup is still clean).

Red first, against the code as merged — the test closes the response without consuming it and asserts the bus has no subscribers left:

$ pytest tests/test_os_events.py -q
        resp = await os_events(req)
        await resp.body_iterator.aclose()

        leaked = {ch: len(qs) for ch, qs in bus._queues.items() if qs}
>       assert leaked == {}, f"leftover bus subscriptions: {leaked}"
E       AssertionError: leftover bus subscriptions: {'user:user-1': 1, 'broadcast': 1}
E       assert {'user:user-1...broadcast': 1} == {}

tests/test_os_events.py:312: AssertionError
FAILED tests/test_os_events.py::test_response_closed_without_streaming_leaks_nothing
1 failed in 0.35s

2 — bounded merged queue

merged is capped at _MERGED_MAXSIZE = 256. Chosen behaviour: drop the oldest, count the drop, and tell the client. When the queue is full the relay discards the oldest buffered event to make room and increments a counter; the generator emits data: {"kind": "events.lagged", "id": null, "ts": ..., "dropped": N} before the next real frame, which is the client's cue to refetch rather than assume it saw everything. The relay never blocks — a blocked relay stalls delivery for the whole connection while the bus keeps filling the two channel queues, which is the "events silently stop" outcome the card rejects.

_relay is now a module-level function taking (src, dst, lag) rather than a closure, so the test drives the real implementation instead of a copy of it. Red proof for the policy, produced by reverting only the relay body to the blocking await dst.put(ev):

$ pytest tests/test_os_events.py::test_relay_drops_oldest_and_counts_the_drop -q
>           await asyncio.wait_for(_until(lambda: src.empty() and lag["dropped"] == 2), 3.0)
>                   raise TimeoutError from exc_val
E                   TimeoutError

/usr/lib/python3.13/asyncio/timeouts.py:116: TimeoutError
FAILED tests/test_os_events.py::test_relay_drops_oldest_and_counts_the_drop
1 failed in 3.30s

3 — dropped the id: line

Removed, along with seq. Emitting an SSE id: is what makes a browser send Last-Event-ID on reconnect, and the module's own docstring says resume is best-effort via the bus replay buffer and not Last-Event-ID; seq also restarted at 1 per connection, so the ids were never stable. The JSON id field (the event's trace id) is untouched — test_stream_delivers_subscribed_kinds still asserts on it.

4 — doc-gate

docs/agent-coordination.md gains a section for the endpoint: auth posture (session cookie, not in EXEMPT_PATHS, no registry scope reaches it), the kinds filter, the frame shape, the no-payload rule, the keepalive, why there is no id: line, the lag behaviour, and why setup lives inside the generator. Not a Docs-Reviewed trailer.

5 — changelog

The direct CHANGELOG.md edit is reverted to dev's copy (git diff origin/dev -- CHANGELOG.md is empty) and replaced with changelog.d/tsk-ronapx-os-events-sse.md.

6 — use-os-events.ts

(a) reconnect when kinds changes. The connect effect depended only on the useCallback identity, which never changed, and the URL is built once per connection — so widening kinds kept streaming the old server-side filter and the new kinds never arrived. The effect is now keyed on the serialized kinds. A companion test pins that a new array with the same contents does not churn the connection.

(b) onerror while CONNECTING. connected=false / stale=true are now set for any error, since an error means events are not arriving whatever the readyState; only a CLOSED stream schedules our own reconnect, because while CONNECTING the browser is already retrying and a second timer would open a second stream.

(c) the mock had no readyState constants. EventSource.CLOSED was undefined on the mock, so the existing disconnect test set readyState = undefined and the hook compared undefined === undefined — it passed no matter what the hook did. The mock now carries CONNECTING/OPEN/CLOSED.

Red proof the card asks for, with the hook's close() removed from the cleanup:

$ npx vitest run src/hooks/use-os-events.test.ts
     × closes the EventSource on unmount 11ms (retry x1)
     × reopens the stream with the new kinds when kinds changes 4ms (retry x1)
 FAIL  src/hooks/use-os-events.test.ts > useOsEvents > closes the EventSource on unmount
AssertionError: expected "vi.fn()" to be called at least once
 FAIL  src/hooks/use-os-events.test.ts > useOsEvents > reopens the stream with the new kinds when kinds changes
AssertionError: expected "vi.fn()" to be called at least once
      Tests  2 failed | 11 passed (13)

And with onerror reverted to the CLOSED-only guard, which is what makes (b) more than a comment:

$ npx vitest run src/hooks/use-os-events.test.ts
     × reports disconnected while the stream is only CONNECTING 12ms (retry x1)
 FAIL  src/hooks/use-os-events.test.ts > useOsEvents > reports disconnected while the stream is only CONNECTING
AssertionError: expected true to be false // Object.is equality
      Tests  1 failed | 12 passed (13)

The broadcast-channel cross-user change is NOT attempted here; it stays with tsk-3q32qc.

Verification

$ pytest tests/test_os_events.py -q                     # 7 passed
$ npx vitest run src/hooks/use-os-events.test.ts        # 13 passed
$ npx tsc --noEmit -p tsconfig.json                     # rc 0
$ python scripts/check_doc_gate.py diff-gate --staged   # rc 0, "doc-gate: clean"
$ python scripts/check_doc_gate.py invariants           # rc 0, "doc-gate: clean"

Summary by CodeRabbit

  • New Features

    • Added authenticated real-time OS event streaming.
    • Supports filtering events by kind and delivers lightweight event metadata.
    • Added connection status, staleness tracking, automatic reconnection, and event deduplication for desktop integrations.
    • Handles reconnect replay, keepalives, buffering, and lag notifications.
  • Documentation

    • Documented the event stream and desktop integration behavior.
  • Tests

    • Added comprehensive coverage for authentication, filtering, reconnection, cleanup, buffering, and multiple subscribers.

jaylfc added 3 commits August 5, 2026 05:04
- New authenticated SSE endpoint at GET /api/os/events streams typed
  change events (kind + id only, never payload) filtered by kinds query param
- New shared client hook useOsEvents(kinds, onEvent) returns connected/stale
  flags, manages single per-client EventSource with exponential backoff
- Reuses existing EventBus SSE plumbing and auth middleware
- Backend tests cover auth gate, kind filtering, and multi-subscriber delivery
- Frontend tests cover hook mount, kind filtering, dedup, stale flag, and reconnect
Supersedes PR #2309 (branch exec/tsk-jmctoa), whose lane is gone. That
branch's endpoint and hook are merged in here on top of current dev, with
the review findings resolved.

- Subscriptions and relay tasks leaked. Both subscribe() calls and both
  create_task() calls ran in the handler body while teardown lived in the
  generator's finally. An async generator closed without ever being
  iterated never runs its body, so a client disconnecting between the
  handler returning and the stream starting leaked two bus subscriptions
  and two tasks, which the bus then fed forever. Setup now happens inside
  the generator, paired with the finally that undoes it.
- The merged queue was unbounded, so a client that stopped reading grew
  the process without limit. It is capped at 256 and the relay drops the
  oldest event rather than blocking, then tells the client with an
  events.lagged frame carrying the drop count.
- Dropped the SSE "id:" line. It is what makes a browser send
  Last-Event-ID, which this endpoint ignores by design, and seq restarted
  at 1 per connection so the ids were not stable anyway.
- Hook: reopen the stream when kinds changes (the URL is fixed per
  connection, so a widened list never arrived), and report disconnected
  on an error while the stream is only CONNECTING.
- The EventSource test mock had no static readyState constants, so the
  disconnect assertions compared undefined to undefined and passed
  whatever the hook did.
- Documented the endpoint in docs/agent-coordination.md and moved the
  direct CHANGELOG.md edit into a changelog.d fragment.
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jaylfc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cce8d1d5-cf11-44eb-9347-3024b2b8c162

📥 Commits

Reviewing files that changed from the base of the PR and between e30b4ed and 533197b.

📒 Files selected for processing (7)
  • changelog.d/tsk-ronapx-os-events-sse.md
  • desktop/src/hooks/use-os-events.test.ts
  • desktop/src/hooks/use-os-events.ts
  • docs/agent-coordination.md
  • tests/test_os_events.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/os_events.py
📝 Walkthrough

Walkthrough

Adds an authenticated /api/os/events SSE endpoint and the useOsEvents desktop hook. The stream emits filtered event metadata without payloads. The hook manages connection state, stale detection, deduplication, kind changes, reconnection, and cleanup.

Changes

OS Events SSE

Layer / File(s) Summary
Authenticated SSE stream
tinyagentos/routes/os_events.py, tinyagentos/routes/__init__.py, tests/test_os_events.py
Adds the authenticated SSE route, EventBus subscriptions, kind filtering, metadata-only frames, keepalives, bounded buffering, lag notifications, cleanup, and backend coverage.
Desktop SSE hook
desktop/src/hooks/use-os-events.ts, desktop/src/hooks/use-os-events.test.ts
Adds useOsEvents with connection and stale state, filtering, event validation, deduplication, backoff reconnects, kind updates, and lifecycle cleanup.
Protocol documentation
docs/agent-coordination.md, changelog.d/tsk-ronapx-os-events-sse.md
Documents the endpoint, frame behavior, buffering, cleanup, and hook behavior. Records the feature in the changelog.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: 🟠 High · up to 27bfc

The SSE stream can still lose requested events under filtered load, hide its own lag signal from clients, and report a stalled connection as healthy indefinitely. These behaviors affect event correctness and connection availability, so the PR is not ready to merge until they are addressed.

Sequence Diagram(s)

sequenceDiagram
  participant Desktop
  participant useOsEvents
  participant OSEvents
  participant EventBus
  Desktop->>useOsEvents: provide kinds and callback
  useOsEvents->>OSEvents: GET /api/os/events?kinds=...
  OSEvents->>EventBus: subscribe to user and broadcast channels
  EventBus-->>OSEvents: matching event
  OSEvents-->>useOsEvents: metadata-only SSE frame
  useOsEvents->>Desktop: deliver validated event
Loading

Possibly related PRs

  • jaylfc/taOS#2220: Introduced the same OS events SSE endpoint, desktop hook, and related tests.
  • jaylfc/taOS#2309: Extended the same endpoint and hook with documentation and additional coverage.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the new SSE endpoint and useOsEvents hook, which are the primary changes.
Linked Issues check ✅ Passed The implementation and tests satisfy the linked issue requirements for the authenticated SSE endpoint, filtering, subscriptions, and useOsEvents behavior [#2309].
Out of Scope Changes check ✅ Passed The changes remain within scope: endpoint, hook, tests, documentation, and changelog updates; broadcast-channel support remains excluded.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 exec/tsk-ronapx

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.

@gitar-bot

gitar-bot Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Gitar is working

Gitar

@jaylfc

jaylfc commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

nemotron-super review

VERDICT: No blocking issues found.

Automated first-pass review by the nemotron-super lane. The lead still reviews before merge.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desktop/src/hooks/use-os-events.ts`:
- Around line 68-80: Update the event handling around OsEvent to expose a
discriminated union for regular events and the events.lagged control event,
validating the lag payload and always forwarding valid lag events regardless of
kinds filtering. Restrict alreadySeen deduplication to regular events with
string IDs, and keep regular events subject to the existing allowlist and
validation; add coverage for lag delivery with both filtered and unfiltered
subscriptions.
- Around line 61-101: Add a named or data heartbeat to the SSE stream and handle
it in the EventSource setup alongside onmessage. Reset a client-side liveness
deadline whenever the heartbeat arrives; on expiry, close the current source,
mark the connection stale, and schedule exactly one reconnect using the existing
reconnect state and cleanup paths in connect and onerror.

In `@tinyagentos/routes/os_events.py`:
- Around line 49-66: Update _relay to accept allowed_kinds and discard events
whose kind is not requested before calling dst.put_nowait(), incrementing no lag
counter for skipped events. Pass the requested filter from the caller and remove
the later post-queue kind filtering so only allowed events consume merged queue
capacity.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 300ed1f7-f683-4a1f-ba47-924fb9f375d1

📥 Commits

Reviewing files that changed from the base of the PR and between e30b4ed and 27bfc74.

📒 Files selected for processing (7)
  • changelog.d/tsk-ronapx-os-events-sse.md
  • desktop/src/hooks/use-os-events.test.ts
  • desktop/src/hooks/use-os-events.ts
  • docs/agent-coordination.md
  • tests/test_os_events.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/os_events.py

Comment on lines +61 to +101
es.onopen = () => {
reconnectAttemptsRef.current = 0;
setConnected(true);
setStale(false);
};

es.onmessage = (msg) => {
let event: OsEvent | null;
try {
event = JSON.parse(msg.data as string) as OsEvent;
} catch {
return;
}
if (!event || typeof event !== "object") return;
if (!event.kind) return;
if (kindsRef.current.length > 0 && !kindsRef.current.includes(event.kind)) {
return;
}
if (alreadySeen(event.id)) return;
onEventRef.current(event);
};

es.onerror = () => {
if (stoppedRef.current) return;
// Whatever the readyState, an error means we are not receiving events.
// Only a CLOSED stream is ours to reconnect: while CONNECTING the
// browser is already retrying, and scheduling our own reconnect on top
// of that would open a second stream.
setConnected(false);
setStale(true);
if (es.readyState === EventSource.CLOSED) {
const delay = Math.min(
RECONNECT_DELAY_MS * 2 ** reconnectAttemptsRef.current,
MAX_RECONNECT_DELAY_MS,
);
reconnectAttemptsRef.current += 1;
reconnectTimerRef.current = setTimeout(() => {
if (!stoppedRef.current) connect();
}, delay);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

For the browser EventSource API, are SSE comment frames delivered to onmessage, and how does EventSource report a silently stalled or half-open connection?

💡 Result:

SSE comment frames, which are lines starting with a colon (e.g., : heartbeat), are not delivered to the onmessage handler or any other event listener in the browser EventSource API [1][2][3]. According to the HTML specification, these lines are ignored by the parser and serve purely as a mechanism to keep the connection alive by preventing intermediate proxies or the browser from timing out an idle stream [1][3][4]. Regarding silently stalled half-open connections, the native EventSource API does not natively detect or report them [5]. If a TCP connection is severed without a proper FIN or RST packet—common in mobile network transitions or with silent proxy timeouts—the EventSource readyState will remain 1 (OPEN) indefinitely, and no error event will be dispatched [5][6]. To handle this, developers must implement a client-side "watchdog" timer pattern [5][6]. This involves the following: 1. Heartbeat: The server must send periodic data or named events (e.g., event: ping) [3][5]. 2. Watchdog Timer: The client resets a timer whenever a message or heartbeat is received [5]. 3. Recovery: If the timer exceeds a predefined threshold (e.g., 30–60 seconds), the client assumes the connection is half-open, calls EventSource.close to clean up the socket, and re-instantiates the EventSource to trigger a reconnection [5]. Relying on the browser's built-in reconnection logic is insufficient for these "zombie" connections because the browser is unaware that the underlying TCP session is no longer functional [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- hook ---'
sed -n '1,180p' desktop/src/hooks/use-os-events.ts
printf '%s\n' '--- related event definitions and producers ---'
rg -n -C 3 'events\.lagged|OsEvent|EventSource|heartbeat|keep-alive|event: ping|: heartbeat' desktop server src .github 2>/dev/null | head -300

Repository: jaylfc/taOS

Length of output: 24448


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- event route and SSE writers ---'
rg -n -C 4 'api/os/events|os/events|text/event-stream|EventSourceResponse|StreamingResponse|events\.lagged|last.?heartbeat|heartbeat' --glob '!desktop/src/hooks/use-os-events.ts' --glob '!desktop/src/hooks/use-os-events.test.ts' .
printf '%s\n' '--- hook tests around lifecycle behavior ---'
sed -n '100,240p' desktop/src/hooks/use-os-events.test.ts

Repository: jaylfc/taOS

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- exact os-events route references ---'
rg -n -C 12 'api/os/events|os/events' --glob '*.py' --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.jsx' .
printf '%s\n' '--- SSE response implementations near exact matches ---'
rg -l 'api/os/events|os/events' --glob '*.py' --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.jsx' . | while IFS= read -r f; do
  echo "### $f"
  rg -n -C 20 'api/os/events|os/events|text/event-stream|StreamingResponse|yield|events\.lagged' "$f"
done

Repository: jaylfc/taOS

Length of output: 41255


Implement observable SSE liveness detection.

The route emits :keepalive comments every 10 seconds, but EventSource does not dispatch comments to onmessage. A silently stalled connection can remain connected: true and stale: false indefinitely.

Send a named or data heartbeat. Reset a client-side deadline when it arrives. When the deadline expires, close the source, set stale, and schedule one reconnect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/hooks/use-os-events.ts` around lines 61 - 101, Add a named or
data heartbeat to the SSE stream and handle it in the EventSource setup
alongside onmessage. Reset a client-side liveness deadline whenever the
heartbeat arrives; on expiry, close the current source, mark the connection
stale, and schedule exactly one reconnect using the existing reconnect state and
cleanup paths in connect and onerror.

Comment thread desktop/src/hooks/use-os-events.ts
Comment thread tinyagentos/routes/os_events.py Outdated
Comment thread tinyagentos/routes/os_events.py Outdated
return JSONResponse({"detail": "Service starting"}, status_code=503)

kinds_param = request.query_params.get("kinds", "")
allowed_kinds = (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Whitespace-only kinds parameter produces an empty allowlist that filters out every event.

The if kinds_param guard treats a whitespace-only string as truthy, so allowed_kinds becomes set() instead of None. When the client sends ?kinds= , the later event.kind not in allowed_kinds check is always True and no events are delivered. This is inconsistent with the documented behaviour ("Omitted or empty means every kind").

Suggested change
allowed_kinds = (
kinds_param = request.query_params.get("kinds", "")
stripped = [k.strip() for k in kinds_param.split(",") if k.strip()]
allowed_kinds = set(stripped) if stripped else None

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (6 files)
  • changelog.d/tsk-ronapx-os-events-sse.md
  • desktop/src/hooks/use-os-events.test.ts
  • desktop/src/hooks/use-os-events.ts
  • docs/agent-coordination.md
  • tests/test_os_events.py
  • tinyagentos/routes/os_events.py
Previous Review Summary (commit 27bfc74)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 27bfc74)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
WARNING 1
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/routes/os_events.py 93 Whitespace-only kinds parameter produces an empty allowlist that filters out every event. The if kinds_param guard treats a whitespace-only string as truthy, so allowed_kinds becomes set() instead of None. When the client sends ?kinds= , no events are delivered, which is inconsistent with the documented behaviour ("Omitted or empty means every kind").
Files Reviewed (7 files)
  • tinyagentos/routes/os_events.py - 1 warning
  • desktop/src/hooks/use-os-events.ts
  • desktop/src/hooks/use-os-events.test.ts
  • tests/test_os_events.py
  • tinyagentos/routes/__init__.py
  • docs/agent-coordination.md
  • changelog.d/tsk-ronapx-os-events-sse.md

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 57.1K · Output: 6.6K · Cached: 220.7K

- kinds filtering moved into the relay. It ran at the yield, after an
  event had already taken one of the 256 slots, so unrelated traffic
  could evict the events a subscriber actually asked for and report a
  lag that, from that subscriber's view, never happened.
- a kinds parameter that names no kind now means "every kind". The guard
  tested the raw string, and "   " is truthy, so the allowlist became an
  empty set that matched nothing and the stream delivered silence. The
  set is derived first and an empty one means no filter.
- events.lagged is a control frame, not a change notification. The hook
  dropped it whenever the caller passed a non-empty kinds list, so the
  one subscriber who needed to know it had missed events never heard;
  and its id is null, so consecutive lag frames collapsed into
  already-seen. It now bypasses both the kind filter and the dedupe, and
  OsEvent.id is typed string | null to match what the server sends.

Each proven red first: the blank-kinds cases fail against the previous
guard, and the lag-delivery test fails without the control-frame branch.
@jaylfc

jaylfc commented Aug 12, 2026 •

Copy link
Copy Markdown
Owner Author

Bot round adjudicated. Both bots reviewed head 27bfc740. Three of the four findings were real and are fixed in 533197bc — two of them defects I introduced with the bounded queue, which is exactly where a new invariant should be attacked.

Fixed.

  • CodeRabbit, os_events.py:66 — "apply the kind filter before adding events to merged." Correct, and mine. _relay enqueued everything and the filter ran at the yield, so an unrequested kind consumed one of the 256 slots first. A busy unrelated kind could therefore evict the events a subscriber actually asked for and trigger an events.lagged that, from that subscriber's point of view, never happened — the bound I added to fix one silent-loss bug had introduced a quieter one. allowed_kinds is now passed to _relay and unwanted kinds are dropped before put_nowait. New test asserts the drop counter stays at 0 while unrequested kinds stream past a full-size queue.

  • CodeRabbit, use-os-events.ts:80 — "handle events.lagged as a control event." Correct, and also mine: I added a frame the client silently discards. The hook drops any kind outside the caller's list, so the one subscriber that most needs the lag signal — a narrow subscriber — never received it. And its id is null, so consecutive lag frames collapsed into "already seen". events.lagged now bypasses both the kind filter and the dedupe, and OsEvent.id is typed string | null to match what the server actually sends. Two tests, one proven red by removing the control-frame branch.

  • Kilo, os_events.py:93 — "whitespace-only kinds filters out every event." Correct, and pre-existing on the superseded branch. if kinds_param treats " " as truthy, so the allowlist became an empty set and event.kind not in set() was always true: the stream connected, kept alive, and delivered nothing. Now the set is derived first and an empty one means "no filter". Proven red on the old guard, parametrized over ?kinds=, ?kinds=%20%20, ?kinds=,:

FAILED tests/test_os_events.py::test_blank_kinds_parameter_delivers_every_kind[kinds=%20%20]
FAILED tests/test_os_events.py::test_blank_kinds_parameter_delivers_every_kind[kinds=%2C]
2 failed, 10 passed

Deferred, not declined — carded as tsk-iiug75.

  • CodeRabbit, use-os-events.ts:101 — "implement observable SSE liveness detection." The facts are right and I checked them independently: comment frames such as :keepalive are not dispatched to onmessage, and a half-open TCP connection leaves readyState at OPEN with no error event, so connected: true / stale: false can persist indefinitely against a dead stream. But the fix is a server change (a real heartbeat event, not a comment) plus a client watchdog plus tests — CodeRabbit labels it a heavy lift and it is one. It is also pre-existing: the :keepalive design arrived with the superseded branch and this PR does not touch it. Widening a supersede PR into that is how these merges stall, so it gets its own card and its own red-first proof rather than a rushed pass here.

Verification after the fixes: 12 backend tests, 15 vitest, tsc rc 0, doc-gate clean both layers. docs/agent-coordination.md and the changelog fragment carry the corrected kinds semantics and the control-frame rule.

@jaylfc

jaylfc commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Head 533197bc folds three findings from your 27bfc740 round plus kilo's — the kind filter moved into the relay, blank-kinds now means every kind, and events.lagged is treated as a control frame that bypasses the caller's filter and the dedupe. Those are production changes no bot has looked at yet, so please review this head rather than the previous one before it merges.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

@jaylfc I will perform a full review of the current head, including the three corrected behaviors.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 15 minutes.

@jaylfc

jaylfc commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Fresh review of head 533197bc came back with no new findings. The two inline comments on this head are re-renders of the previous round: the events.lagged control-frame one carries CodeRabbit's own ✅ Addressed in commit 533197b, and the SSE liveness one is the item deferred to tsk-iiug75 (unchanged here on purpose — it needs a server heartbeat plus a client watchdog and its own red-first proof).

Recorded rather than glossed: kilo's check reads SUCCESS at this head with an empty description, which is the run completing, not a review artifact. The substantive re-review here is CodeRabbit's. Merging on that plus my own line review of the three folded fixes, each proven red first.

@jaylfc
jaylfc merged commit 7531a5f into dev Aug 12, 2026
21 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.

1 participant