Skip to content

Fix-forward PR 2220 (os-events SSE): doc-gate + hook fixes - #2309

Closed
jaylfc wants to merge 1 commit into
devfrom
exec/tsk-jmctoa
Closed

jaylfc wants to merge 1 commit into
devfrom
exec/tsk-jmctoa

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

CARD TITLE (intent, not commit subject): Fix-forward PR 2220 (os-events SSE): doc-gate + hook fixes

Autonomous build of board card tsk-jmctoa.

  • 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

Files:
CHANGELOG.md | 6 +
desktop/src/hooks/use-os-events.test.ts | 228 ++++++++++++++++++++++++++
desktop/src/hooks/use-os-events.ts | 113 +++++++++++++
tests/test_os_events.py | 282 ++++++++++++++++++++++++++++++++
tinyagentos/routes/init.py | 3 +
tinyagentos/routes/os_events.py | 121 ++++++++++++++
6 files changed, 753 insertions(+)

Summary by CodeRabbit

  • New Features

    • Added an authenticated, real-time operating-system event stream.
    • Events include identifiers, types, and timestamps while omitting payload details.
    • Added reliable connection monitoring with freshness status and automatic reconnection after interruptions.
    • Supports filtering for relevant event types and safely handles duplicate or invalid events.
  • Documentation

    • Added an Unreleased changelog entry describing the new event-stream capabilities.

- 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
@gitar-bot

gitar-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added an authenticated /api/os/events SSE endpoint and registered it with the application. Added the useOsEvents hook for filtered event delivery, connection status, deduplication, and exponential-backoff reconnection. Added backend and desktop tests plus changelog documentation.

Changes

OS events streaming

Layer / File(s) Summary
Backend SSE stream
tinyagentos/routes/os_events.py
The endpoint authenticates requests, subscribes to user and broadcast channels, filters event kinds, emits payload-free metadata, sends keepalives, and cleans up on disconnect.
Backend integration and validation
tinyagentos/routes/__init__.py, tests/test_os_events.py
The router is registered with CSRF handling. Tests cover authentication, filtering, metadata formatting, fan-out, and cleanup.
Desktop event hook
desktop/src/hooks/use-os-events.ts, CHANGELOG.md
useOsEvents maintains one EventSource connection, filters and deduplicates events, exposes connected and stale, reconnects with capped exponential backoff, and updates the changelog.
Desktop hook validation
desktop/src/hooks/use-os-events.test.ts
Tests cover URL construction, event handling, connection state, cleanup, duplicate IDs, reconnection, and missing event kinds.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Desktop as Desktop client
  participant Hook as useOsEvents
  participant API as /api/os/events
  participant Bus as EventBus

  Desktop->>Hook: Request event kinds
  Hook->>API: Open filtered EventSource connection
  API->>Bus: Subscribe to user and broadcast channels
  Bus-->>API: Deliver SystemEvent
  API-->>Hook: Send payload-free SSE metadata
  Hook-->>Desktop: Invoke event handler
  API-->>Hook: Close connection
  Hook->>API: Reconnect with exponential backoff
Loading

Possibly related PRs

  • jaylfc/taOS#2220: Contains the same OS events SSE endpoint, useOsEvents hook, tests, and router registration changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.89% 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 clearly identifies the OS events SSE change and related hook fixes, which match the main pull request objectives.
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.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exec/tsk-jmctoa

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add authenticated OS events SSE stream and useOsEvents client hook

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add authenticated GET /api/os/events SSE stream emitting kind/id-only OS change events.
• Introduce useOsEvents(kinds, onEvent) hook with filtering, dedupe, stale/connected flags, and
 backoff reconnect.
• Add backend + frontend test coverage and document the feature in the changelog.
Diagram

sequenceDiagram
  participant UI as "Desktop UI"
  participant Hook as "useOsEvents"
  participant ES as "EventSource"
  participant API as "GET /api/os/events"
  participant Auth as "AuthMiddleware"
  participant Bus as "EventBus"

  UI->>Hook: mount(kinds, onEvent)
  Hook->>ES: new(url?kinds=...)
  ES->>API: open SSE connection
  API->>Auth: validate session/user_id
  Auth-->>API: user_id
  API->>Bus: subscribe(user:<id>, broadcast)
  Bus-->>API: replay + live SystemEvents
  API-->>ES: data {kind,id,ts}
  ES-->>Hook: message(event)
  Hook-->>UI: onEvent(event)
  ES-->>Hook: error + readyState=CLOSED
  Hook->>ES: reconnect (exponential backoff)
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extend existing `/api/events/stream` with a minimal + kinds-filter mode
  • ➕ Avoids duplicating the SSE merge/keepalive/unsubscribe logic across two routes
  • ➕ Single SSE endpoint simplifies client/network observability and ops knobs
  • ➖ Needs careful hardening to guarantee payload is never emitted in minimal mode
  • ➖ Expands the contract of an existing endpoint used for notification routing
2. Provide a shared singleton connection multiplexer on the client
  • ➕ Truly enforces “single per-client connection” even if multiple components subscribe
  • ➕ Centralizes reconnect/backoff/dedupe and fans out to multiple subscribers
  • ➖ More complex hook API (subscribe/unsubscribe bookkeeping) and test surface
  • ➖ Higher coupling across features that want OS events

Recommendation: The PR’s split endpoint (/api/os/events) is a defensible security boundary because it guarantees payload-free events by construction. Consider a follow-up refactor to share server-side SSE plumbing with event_stream.py (or add a minimal mode there) to reduce duplication, and clarify whether useOsEvents is intended to be mounted once or to support multi-subscriber fan-out via a singleton connection.

Files changed (6) +753 / -0

Enhancement (3) +237 / -0
use-os-events.tsAdd 'useOsEvents' SSE hook with backoff reconnect and dedupe +113/-0

Add 'useOsEvents' SSE hook with backoff reconnect and dedupe

• Implements a React hook that opens an 'EventSource' to '/api/os/events' (optionally with 'kinds'), filters incoming messages, deduplicates by event id with a bounded cache, and exposes 'connected'/'stale' state with exponential-backoff reconnect on hard closes.

desktop/src/hooks/use-os-events.ts

__init__.pyRegister OS events router in application setup +3/-0

Register OS events router in application setup

• Includes the new 'tinyagentos.routes.os_events' router during app router registration with the existing CSRF dependency wiring.

tinyagentos/routes/init.py

os_events.pyAdd authenticated SSE endpoint '/api/os/events' emitting kind/id-only events +121/-0

Add authenticated SSE endpoint '/api/os/events' emitting kind/id-only events

• Implements a FastAPI SSE endpoint that requires 'request.state.user_id', subscribes to per-user and broadcast EventBus channels, optionally filters by 'kinds' query param, sends keepalives, and streams JSON frames containing only 'kind', 'id' (trace_id), and 'ts' (no payload).

tinyagentos/routes/os_events.py

Tests (2) +510 / -0
use-os-events.test.tsAdd unit tests for 'useOsEvents' filtering, dedupe, and reconnect +228/-0

Add unit tests for 'useOsEvents' filtering, dedupe, and reconnect

• Introduces a mocked 'EventSource' test harness and verifies URL construction, kind filtering, id deduplication, connected/stale flags, unmount cleanup, and reconnect behavior after hard disconnect.

desktop/src/hooks/use-os-events.test.ts

test_os_events.pyAdd API tests for '/api/os/events' auth and kind filtering +282/-0

Add API tests for '/api/os/events' auth and kind filtering

• Adds async tests that assert the stream rejects unauthenticated clients, only delivers subscribed kinds, excludes payloads from delivered frames, and fans out to multiple subscribers.

tests/test_os_events.py

Documentation (1) +6 / -0
CHANGELOG.mdDocument new OS events SSE endpoint and 'useOsEvents' hook +6/-0

Document new OS events SSE endpoint and 'useOsEvents' hook

• Adds a changelog entry describing the authenticated '/api/os/events' SSE stream and the 'useOsEvents(kinds, onEvent)' hook, including kind/id-only payload policy and reconnect/health behavior.

CHANGELOG.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (2)

Grey Divider


Action required

1. Kinds change not applied 🐞 Bug ≡ Correctness
Description
useOsEvents updates kindsRef when kinds changes but never recreates the EventSource URL, so
expanding the kinds list can permanently miss events filtered out by the server. The hook will
keep streaming the original server-side subscription until an unmount/remount (or hard-close
reconnect) occurs.
Code

desktop/src/hooks/use-os-events.ts[R94-97]

+        }, delay);
+      }
+    };
+  }, []);
Relevance

●● Moderate

Hook correctness fix seems plausible, but no close precedent about reconnecting EventSource on kinds
change.

PR-#470

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The hook records new kinds into a ref but the only connect() call is in a mount-only effect
because connect is stable (useCallback(..., [])), so the EventSource URL cannot change when
kinds changes.

desktop/src/hooks/use-os-events.ts[34-37]
desktop/src/hooks/use-os-events.ts[38-47]
desktop/src/hooks/use-os-events.ts[97-110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useOsEvents(kinds, onEvent)` keeps the EventSource URL fixed to whatever `kinds` were at initial mount because `connect` is memoized with `[]` and only called once. Updating `kindsRef.current` only affects client-side filtering and cannot recover events the server never sent.

## Issue Context
The server endpoint supports `?kinds=...` filtering. If a component adds kinds later, the server continues filtering with the old query string, so new kinds never arrive.

## Fix Focus Areas
- desktop/src/hooks/use-os-events.ts[34-45]
- desktop/src/hooks/use-os-events.ts[94-110]

## Suggested fix
- Derive a stable `kindsKey` (e.g., sorted/joined string) from `kinds`.
- When `kindsKey` changes, close the existing `EventSource`, clear any pending reconnect timer, and open a new `EventSource` with the updated URL.
- Ensure you don’t create parallel connections (guard so only one active ES exists at a time).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Missing os_events docs update 📜 Skill insight § Compliance
Description
A new route module tinyagentos/routes/os_events.py was added and registered, but
docs/agent-coordination.md was not updated to document GET /api/os/events (and no
Docs-Reviewed: trailer is visible in this PR diff). This violates the doc-gate requirement for
route module changes.
Code

tinyagentos/routes/init.py[R350-351]

+    from tinyagentos.routes.os_events import router as os_events_router
+    app.include_router(os_events_router, dependencies=_csrf)
Relevance

●●● Strong

Docs fixes are commonly accepted; this PR’s stated intent includes doc-gate compliance.

PR-#482

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR adds and registers the new os_events router, triggering the doc-gate for route module changes.
The existing coordination doc discusses SSE endpoints but contains no mention of /api/os/events,
indicating the required documentation update was not made in this PR diff.

tinyagentos/routes/init.py[350-351]
docs/agent-coordination.md[113-115]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new route module was added/registered (`GET /api/os/events`), but `docs/agent-coordination.md` does not mention it. The doc-gate rule requires either updating the docs or adding a `Docs-Reviewed:` commit trailer when no user-facing doc change is needed.

## Issue Context
`tinyagentos/routes/os_events.py` introduces a new authenticated SSE endpoint and `tinyagentos/routes/__init__.py` registers it.

## Fix Focus Areas
- tinyagentos/routes/__init__.py[350-351]
- docs/agent-coordination.md[113-115]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Empty kinds filters everything 🐞 Bug ≡ Correctness
Description
Backend kinds parsing treats inputs like kinds=, as an empty set (not “all kinds”), causing the
stream to drop every event while still returning 200/SSE. This contradicts the endpoint contract
that empty/missing kinds means “subscribe to all”.
Code

tinyagentos/routes/os_events.py[R58-61]

+    kinds_param = request.query_params.get("kinds", "")
+    allowed_kinds = (
+        {k.strip() for k in kinds_param.split(",") if k.strip()}
+        if kinds_param
Relevance

●●● Strong

Deterministic query-parse edge case; small correctness hardening in routes is usually accepted.

PR-#280

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
allowed_kinds is computed as a set whenever kinds_param is truthy; for kinds=, the parsed set
is empty, and the generator filters on membership, dropping every event.

tinyagentos/routes/os_events.py[58-64]
tinyagentos/routes/os_events.py[90-95]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`allowed_kinds` becomes an empty set for syntactically-nonempty but semantically-empty values like `kinds=,` or `kinds=,,`, which makes the generator filter out all events.

## Issue Context
Docs in the os_events route state that an empty or missing `kinds` parameter means "subscribe to all".

## Fix Focus Areas
- tinyagentos/routes/os_events.py[58-64]
- tinyagentos/routes/os_events.py[93-95]

## Suggested fix
- Parse kinds into `parsed = { ... }`.
- Set `allowed_kinds = parsed or None` so empty parses behave like missing/empty.
- Optionally: return 400 for invalid values if you want strictness, but keep behavior aligned with documented "empty means all".

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Unbounded per-client queue 🐞 Bug ☼ Reliability
Description
The SSE handler drains EventBus subscriber queues into an unbounded merged queue; under sustained
event rates and slow/blocked response consumption, this buffer can grow without bound and increase
process memory. This creates a per-connection reliability/DoS risk.
Code

tinyagentos/routes/os_events.py[R69-72]

+    # Merge both channels into a single queue so the generator has one await.
+    merged: asyncio.Queue = asyncio.Queue()
+
+    async def _relay(src: asyncio.Queue) -> None:
Relevance

●●● Strong

SSE/queue reliability concerns have been accepted before; bounding/handling backlog risk likely
accepted.

PR-#485

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The route creates an unbounded merged queue and relays every event into it; EventBus queues are
also unbounded and publishers push without awaiting, so buffering can accumulate in-memory per
connection when consumption can’t keep up.

tinyagentos/routes/os_events.py[69-76]
tinyagentos/routes/os_events.py[82-95]
tinyagentos/events/bus.py[51-61]
tinyagentos/events/bus.py[69-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`merged` is created with default (unbounded) capacity and relay tasks continuously enqueue into it. If the downstream StreamingResponse iteration is backpressured or stalled, events can accumulate indefinitely.

## Issue Context
EventBus subscriber queues are also unbounded and are fed via `put_nowait`, so there is no natural backpressure in the publisher path.

## Fix Focus Areas
- tinyagentos/routes/os_events.py[69-76]
- tinyagentos/routes/os_events.py[82-110]
- tinyagentos/events/bus.py[51-61]
- tinyagentos/events/bus.py[69-75]

## Suggested fix
- Make `merged` a bounded queue (e.g., `asyncio.Queue(maxsize=256)` or similar).
- In `_relay`, use `put_nowait` and implement an explicit overflow policy:
 - drop-newest (skip enqueue when full), or
 - drop-oldest (e.g., `merged.get_nowait()` then `put_nowait()`), or
 - disconnect the client when it falls behind.
- Add a small log/metric when dropping so it’s observable in production.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. os_events returns raw dicts 📜 Skill insight ✧ Quality
Description
os_events() returns error responses using raw dicts via JSONResponse instead of using Pydantic
response models. This reduces schema clarity and violates the requirement to use Pydantic models for
response payloads.
Code

tinyagentos/routes/os_events.py[R50-56]

+    user_id = getattr(request.state, "user_id", None)
+    if not user_id:
+        return JSONResponse({"detail": "Unauthorized"}, status_code=401)
+
+    event_bus = getattr(request.app.state, "event_bus", None)
+    if event_bus is None:
+        return JSONResponse({"detail": "Service starting"}, status_code=503)
Relevance

● Weak

Close precedent: team rejected switching raw dict responses to Pydantic response models.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance checklist requires Pydantic models for response payloads. The handler currently
constructs response bodies as raw dicts ({"detail": ...}) when returning 401/503 responses.

tinyagentos/routes/os_events.py[50-56]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `GET /api/os/events` handler returns error JSON bodies as untyped dicts via `JSONResponse`, rather than using Pydantic models / declared response schemas.

## Issue Context
Compliance requires route request/response payloads to use Pydantic models for validation and documented schemas.

## Fix Focus Areas
- tinyagentos/routes/os_events.py[50-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 35 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +350 to +351
from tinyagentos.routes.os_events import router as os_events_router
app.include_router(os_events_router, dependencies=_csrf)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Missing os_events docs update 📜 Skill insight § Compliance

A new route module tinyagentos/routes/os_events.py was added and registered, but
docs/agent-coordination.md was not updated to document GET /api/os/events (and no
Docs-Reviewed: trailer is visible in this PR diff). This violates the doc-gate requirement for
route module changes.
Agent Prompt
## Issue description
A new route module was added/registered (`GET /api/os/events`), but `docs/agent-coordination.md` does not mention it. The doc-gate rule requires either updating the docs or adding a `Docs-Reviewed:` commit trailer when no user-facing doc change is needed.

## Issue Context
`tinyagentos/routes/os_events.py` introduces a new authenticated SSE endpoint and `tinyagentos/routes/__init__.py` registers it.

## Fix Focus Areas
- tinyagentos/routes/__init__.py[350-351]
- docs/agent-coordination.md[113-115]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +94 to +97
}, 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.

Action required

3. Kinds change not applied 🐞 Bug ≡ Correctness

useOsEvents updates kindsRef when kinds changes but never recreates the EventSource URL, so
expanding the kinds list can permanently miss events filtered out by the server. The hook will
keep streaming the original server-side subscription until an unmount/remount (or hard-close
reconnect) occurs.
Agent Prompt
## Issue description
`useOsEvents(kinds, onEvent)` keeps the EventSource URL fixed to whatever `kinds` were at initial mount because `connect` is memoized with `[]` and only called once. Updating `kindsRef.current` only affects client-side filtering and cannot recover events the server never sent.

## Issue Context
The server endpoint supports `?kinds=...` filtering. If a component adds kinds later, the server continues filtering with the old query string, so new kinds never arrive.

## Fix Focus Areas
- desktop/src/hooks/use-os-events.ts[34-45]
- desktop/src/hooks/use-os-events.ts[94-110]

## Suggested fix
- Derive a stable `kindsKey` (e.g., sorted/joined string) from `kinds`.
- When `kindsKey` changes, close the existing `EventSource`, clear any pending reconnect timer, and open a new `EventSource` with the updated URL.
- Ensure you don’t create parallel connections (guard so only one active ES exists at a time).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +58 to +61
kinds_param = request.query_params.get("kinds", "")
allowed_kinds = (
{k.strip() for k in kinds_param.split(",") if k.strip()}
if kinds_param

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Empty kinds filters everything 🐞 Bug ≡ Correctness

Backend kinds parsing treats inputs like kinds=, as an empty set (not “all kinds”), causing the
stream to drop every event while still returning 200/SSE. This contradicts the endpoint contract
that empty/missing kinds means “subscribe to all”.
Agent Prompt
## Issue description
`allowed_kinds` becomes an empty set for syntactically-nonempty but semantically-empty values like `kinds=,` or `kinds=,,`, which makes the generator filter out all events.

## Issue Context
Docs in the os_events route state that an empty or missing `kinds` parameter means "subscribe to all".

## Fix Focus Areas
- tinyagentos/routes/os_events.py[58-64]
- tinyagentos/routes/os_events.py[93-95]

## Suggested fix
- Parse kinds into `parsed = { ... }`.
- Set `allowed_kinds = parsed or None` so empty parses behave like missing/empty.
- Optionally: return 400 for invalid values if you want strictness, but keep behavior aligned with documented "empty means all".

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +69 to +72
# Merge both channels into a single queue so the generator has one await.
merged: asyncio.Queue = asyncio.Queue()

async def _relay(src: asyncio.Queue) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Unbounded per-client queue 🐞 Bug ☼ Reliability

The SSE handler drains EventBus subscriber queues into an unbounded merged queue; under sustained
event rates and slow/blocked response consumption, this buffer can grow without bound and increase
process memory. This creates a per-connection reliability/DoS risk.
Agent Prompt
## Issue description
`merged` is created with default (unbounded) capacity and relay tasks continuously enqueue into it. If the downstream StreamingResponse iteration is backpressured or stalled, events can accumulate indefinitely.

## Issue Context
EventBus subscriber queues are also unbounded and are fed via `put_nowait`, so there is no natural backpressure in the publisher path.

## Fix Focus Areas
- tinyagentos/routes/os_events.py[69-76]
- tinyagentos/routes/os_events.py[82-110]
- tinyagentos/events/bus.py[51-61]
- tinyagentos/events/bus.py[69-75]

## Suggested fix
- Make `merged` a bounded queue (e.g., `asyncio.Queue(maxsize=256)` or similar).
- In `_relay`, use `put_nowait` and implement an explicit overflow policy:
  - drop-newest (skip enqueue when full), or
  - drop-oldest (e.g., `merged.get_nowait()` then `put_nowait()`), or
  - disconnect the client when it falls behind.
- Add a small log/metric when dropping so it’s observable in production.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@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: 6

🤖 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 `@CHANGELOG.md`:
- Around line 20-25: Update the CHANGELOG entry for the OS-level typed
change-event stream and useOsEvents to document the complete event schema,
including the ts metadata field alongside kind and id; keep the description
consistent with the existing OsEvent type and producer behavior.

In `@desktop/src/hooks/use-os-events.ts`:
- Around line 34-36: Update the EventSource connection logic in the useOsEvents
hook so changes to kinds recreate the server subscription using a stable
serialized kind key as the effect dependency, rather than only updating
kindsRef.current. Preserve local filtering and ensure cleanup closes the
previous connection; add a rerender test that confirms the new URL and delivery
of events for the updated kind.
- Around line 83-95: Update the EventSource onerror handler in
desktop/src/hooks/use-os-events.ts (lines 83-95) to handle CONNECTING-state
errors like active-source failures: mark the hook disconnected and stale, close
es, and schedule one guarded retry without duplicate timers. In
desktop/src/hooks/use-os-events.test.ts (lines 25-40), expose CONNECTING, OPEN,
and CLOSED on the mock constructor; update lines 112-135 to cover the CONNECTING
error path, and lines 165-213 to reserve the CLOSED path for fatal closure
behavior.

In `@tests/test_os_events.py`:
- Around line 197-203: Update the negative event-filtering test around the
stream task: after the timeout, assert that task is still running before
cancelling it, then await the cancelled task while suppressing only
asyncio.CancelledError. Do not catch Exception, so unexpected stream failures
propagate instead of allowing the empty-lines assertion to pass.

In `@tinyagentos/routes/os_events.py`:
- Around line 96-103: Keep the SSE payload serialized by the event-stream
generator limited to kind and id by removing ts from the data object in
tinyagentos/routes/os_events.py lines 96-103. Update the corresponding
assertions in tests/test_os_events.py lines 132-137 and 275-282 to remove
expectations for ts.
- Around line 70-80: Update the EventBus subscription path used by the SSE
response and the _relay flow in os_events.py to give each subscriber bounded
buffering instead of an unbounded merged queue. Define and apply the existing or
appropriate drop/coalesce policy for events when a slow client reaches capacity,
ensuring backpressure or dropping occurs at the per-subscriber boundary rather
than merely accumulating upstream.
🪄 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: 74ef26cb-cd48-419a-a6db-afc9cdb34e4c

📥 Commits

Reviewing files that changed from the base of the PR and between 94cbb74 and 7902375.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • desktop/src/hooks/use-os-events.test.ts
  • desktop/src/hooks/use-os-events.ts
  • tests/test_os_events.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/os_events.py

Comment thread CHANGELOG.md
Comment on lines +20 to +25
- **OS-level typed change-event stream + `useOsEvents` hook.** A new authenticated
SSE endpoint (`GET /api/os/events`) streams typed change events carrying only the
event kind and id, never the payload, so apps can opt into live updates with a
single hook call. The shared `useOsEvents(kinds, onEvent)` hook manages a single
per-client connection, exposes `connected` and `stale` flags, and handles
reconnect with exponential backoff.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the documented event schema.

The hook exposes OsEvent.ts, and the producer sends ts. The entry says that events carry only kind and id.

Document ts as metadata, or remove it from the producer and client type.

🤖 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 `@CHANGELOG.md` around lines 20 - 25, Update the CHANGELOG entry for the
OS-level typed change-event stream and useOsEvents to document the complete
event schema, including the ts metadata field alongside kind and id; keep the
description consistent with the existing OsEvent type and producer behavior.

Comment on lines +34 to +36
useEffect(() => {
kindsRef.current = kinds;
}, [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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reconnect when kinds changes.

The server filters the SSE stream when it creates the connection. Updating only kindsRef.current leaves the server subscribed to the old kind list.

If a caller changes kinds from ["a"] to ["b"], the client drops a events locally but never receives b events. Recreate the EventSource when a stable serialized kind key changes. Add a rerender test that verifies the new URL and delivery for the new kind.

Also applies to: 99-110

🤖 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 34 - 36, Update the
EventSource connection logic in the useOsEvents hook so changes to kinds
recreate the server subscription using a stable serialized kind key as the
effect dependency, rather than only updating kindsRef.current. Preserve local
filtering and ensure cleanup closes the previous connection; add a rerender test
that confirms the new URL and delivery of events for the updated kind.

Comment on lines +83 to +95
es.onerror = () => {
if (!stoppedRef.current && es.readyState === EventSource.CLOSED) {
const delay = Math.min(
RECONNECT_DELAY_MS * 2 ** reconnectAttemptsRef.current,
MAX_RECONNECT_DELAY_MS,
);
reconnectAttemptsRef.current += 1;
setConnected(false);
setStale(true);
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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to the current WHATWG HTML EventSource standard, does a recoverable connection failure dispatch errorwithreadyStateset toCONNECTING, while CLOSED prevents automatic reconnection?

💡 Result:

According to the current WHATWG HTML Living Standard for Server-Sent Events, the behavior is as follows: When a recoverable connection failure occurs (such as a transient network issue), the user agent attempts to reestablish the connection [1][2]. During this process, the EventSource object's readyState is set to CONNECTING (value 0) [3][4][2]. An error event is dispatched, allowing the application to observe the failure even though the connection attempt is ongoing [5][6]. Conversely, when the user agent determines that it must "fail the connection" (typically due to a fatal error, such as a 204 No Content response, an incorrect Content-Type, or when the close method is invoked), it sets the readyState to CLOSED (value 2) and dispatches an error event [4][2]. Once the connection has reached the CLOSED state in this manner, the user agent does not attempt to reconnect [1][4][2]. Therefore, your understanding is correct: a recoverable failure involves the readyState being set to CONNECTING during the reconnection process [4][2], while a transition to the CLOSED state signifies a permanent termination of the connection, preventing any further automatic reconnection [1][4][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files | rg '^desktop/src/hooks/use-os-events(\.test)?\.ts$' || true

printf '\nMain hook outline:\n'
ast-grep outline desktop/src/hooks/use-os-events.ts --view expanded || true

printf '\nMain hook relevant lines:\n'
sed -n '1,140p' desktop/src/hooks/use-os-events.ts | nl -ba

printf '\n Test relevant lines:\n'
sed -n '1,240p' desktop/src/hooks/use-os-events.test.ts | nl -ba

printf '\n Deterministic condition check:\n'
python3 - <<'PY'
import re
from pathlib import Path
code = Path('desktop/src/hooks/use-os-events.ts').read_text()
m = re.search(r'es\.onerror\s*=\s*\(\)\s*=>\s*\{(?P<body>.*?)\n\s*\};', code, re.S)
print('onerror body exists:', bool(m))
if m:
    body = m.group('body')
    print('uses CLOSED guard:', 'EventSource.CLOSED' in body or 'CLOSED' in body)
    print('sets stale:', 'setStale(true)' in body)
    print('sets connected false:', 'setConnected(false)' in body)
    print('calls close:', 'es.close()' in body)
    print('timeout/reconnect:', ('setTimeout' in body) and ('reconnectTimerRef.current' in body))
PY

Repository: jaylfc/taOS

Length of output: 599


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files | rg '^desktop/src/hooks/use-os-events(\.test)?\.ts$' || true

printf '\nMain hook outline:\n'
ast-grep outline desktop/src/hooks	use-os-events.ts --view expanded || true

printf '\nMain hook relevant lines:\n'
sed -n '1,140p' desktop/src/hooks/use-os-events.ts | nl -ba

printf '\n Test relevant lines:\n'
sed -n '1,240p' desktop/src/hooks/use-os-events.test.ts | nl -ba

printf '\n Deterministic condition check:\n'
python3 - <<'PY'
import re
from pathlib import Path
code = Path('desktop/src/hooks/use-os-events.ts').read_text()
m = re.search(r'es\.onerror\s*=\s*\(\)\s*=>\s*\{(?P<body>.*?)\n\s*\};', code, re.S)
print('onerror body exists:', bool(m))
if m:
    body = m.group('body')
    print('uses CLOSED guard:', 'EventSource.CLOSED' in body or 'CLOSED' in body)
    print('sets stale:', 'setStale(true)' in body)
    print('sets connected false:', 'setConnected(false)' in body)
    print('calls close:', 'es.close()' in body)
    print('timeout/reconnect:', ('setTimeout' in body) and ('reconnectTimerRef.current' in body))
PY

Repository: jaylfc/taOS

Length of output: 5460


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' desktop/src/hooks/use-os-events.ts
printf '\n--- test 1-240 ---\n'
sed -n '1,240p' desktop/src/hooks/use-os-events.test.ts

printf '\n--- onerror condition extraction ---\n'
python3 - <<'PY'
from pathlib import Path
import re
code = Path('desktop/src/hooks/use-os-events.ts').read_text()
m = re.search(r'es\.onerror\s*=\s*\(\)\s*=>\s*\{(?P<body>.*?)\n\s*\};', code, re.S)
if m:
    body = m.group(0)
    print(body)
    print('CLOSED guard present:', 'EventSource.CLOSED' in body or 'CLOSED' in body)
    print('sets stale in error handler:', bool(re.search(r'setStale\s*\(\s*true\s*\)', body)))
    print('sets connected false in error handler:', bool(re.search(r'setConnected\s*\(\s*false\s*\)', body)))
    print('calls es.close in error handler:', bool(re.search(r'es\.close\s*\(', body)))
    print('schedules reconnect in error handler:', bool(re.search(r'reconnectTimerRef\.current\s*=|setTimeout\s*\(', body)))
PY

Repository: jaylfc/taOS

Length of output: 9993


🌐 Web query:

WHATWG HTML EventSource readyState CLOSED reestablish connection error even though readyState is not CLOSED

💡 Result:

In the WHATWG HTML standard, the EventSource API behavior regarding the readyState and reconnection is precisely defined [1][2]. The perception of a mismatch—where a connection error occurs but readyState is not CLOSED—is typically a result of how the browser natively manages automatic reconnections [3][4]. When an EventSource connection encounters an error (such as a temporary network drop or a clean TCP disconnect), the browser fires an error event and automatically transitions the readyState to CONNECTING (0) to initiate an automatic retry [1][4]. Because the browser is actively attempting to reconnect, the readyState will be CONNECTING rather than CLOSED (2) [2][4]. The CLOSED state (2) is reserved for situations where the connection is permanently terminated [2]. According to the standard, the browser stops retrying and sets readyState to CLOSED only in specific, fatal scenarios [2][4]: 1. Explicit Client Action: The user calls the.close method [5][2][3]. 2. Server Rejection: The server returns specific HTTP error codes (often 4xx or 5xx, depending on implementation) or provides an incorrect MIME type (anything other than text/event-stream) [3][4]. 3. CORS Failures: Failures during the CORS preflight process [4]. If you observe an error event but the readyState is not CLOSED, it indicates that the browser's native retry mechanism is currently active [4]. If your intention is to stop these automatic retries, you must manually call.close within your error event listener [3][4]. Conversely, if you are experiencing a state where the connection is clearly dead but the readyState remains OPEN (1), this is often due to a delay in the browser detecting the TCP/network failure, rather than a failure of the EventSource specification itself [4][6].

Citations:


Handle recoverable EventSource error events.

A recoverable EventSource error dispatches error while the browser native connects in CONNECTING state. The current error handler only runs when readyState === EventSource.CLOSED, so normal connection loss leaves connected=true and stale=false and skips this hook’s retry schedule.

  • desktop/src/hooks/use-os-events.ts#L83-L95: treat active-source errors the same way: set stale/disconnected, call es.close(), and schedule one guarded retry.
  • desktop/src/hooks/use-os-events.test.ts#L25-L40: expose EventSource.CONNECTING, OPEN, and CLOSED on the mock constructor.
  • desktop/src/hooks/use-os-events.test.ts#L112-L135 / L165-L213: exercise the CONNECTING error path, then reuse the CLOSED path only for fatal closure behavior.
📍 Affects 2 files
  • desktop/src/hooks/use-os-events.ts#L83-L95 (this comment)
  • desktop/src/hooks/use-os-events.test.ts#L25-L40
  • desktop/src/hooks/use-os-events.test.ts#L112-L135
  • desktop/src/hooks/use-os-events.test.ts#L165-L213
🤖 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 83 - 95, Update the
EventSource onerror handler in desktop/src/hooks/use-os-events.ts (lines 83-95)
to handle CONNECTING-state errors like active-source failures: mark the hook
disconnected and stale, close es, and schedule one guarded retry without
duplicate timers. In desktop/src/hooks/use-os-events.test.ts (lines 25-40),
expose CONNECTING, OPEN, and CLOSED on the mock constructor; update lines
112-135 to cover the CONNECTING error path, and lines 165-213 to reserve the
CLOSED path for fatal closure behavior.

Comment thread tests/test_os_events.py
Comment on lines +197 to +203
task.cancel()
try:
await task
except (asyncio.CancelledError, Exception):
pass

assert not lines, f"expected no events for wrong kind, got: {lines}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not suppress stream task failures in the negative test.

Lines 197-201 discard an exception from the application task. If the stream fails before delivery, lines remains empty and Line 203 passes. The test then does not prove that filtering works.

After the timeout, assert that task is still running before cancellation. During cleanup, suppress only the expected asyncio.CancelledError that follows task.cancel().

🧰 Tools
🪛 Ruff (0.16.1)

[error] 200-201: try-except-pass detected, consider logging the exception

(S110)


[warning] 200-200: Do not catch blind exception: Exception

(BLE001)

🤖 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 `@tests/test_os_events.py` around lines 197 - 203, Update the negative
event-filtering test around the stream task: after the timeout, assert that task
is still running before cancelling it, then await the cancelled task while
suppressing only asyncio.CancelledError. Do not catch Exception, so unexpected
stream failures propagate instead of allowing the empty-lines assertion to pass.

Source: Linters/SAST tools

Comment on lines +70 to +80
merged: asyncio.Queue = asyncio.Queue()

async def _relay(src: asyncio.Queue) -> None:
while True:
ev = await src.get()
await merged.put(ev)

relay_tasks = [
asyncio.create_task(_relay(user_q), name="os-events-relay-user"),
asyncio.create_task(_relay(bcast_q), name="os-events-relay-bcast"),
]

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

Bound buffering for slow SSE clients.

Lines 70-80 relay every event into an unbounded merged queue. If a connected client stops reading, the response generator stops draining while both relay tasks continue adding events. Each such connection can consume process memory without limit.

Use bounded per-subscriber buffering and define drop or coalesce behavior for slow consumers. Apply the limit through the EventBus subscription path so blocked relay tasks do not only move the unbounded backlog upstream.

🤖 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 `@tinyagentos/routes/os_events.py` around lines 70 - 80, Update the EventBus
subscription path used by the SSE response and the _relay flow in os_events.py
to give each subscriber bounded buffering instead of an unbounded merged queue.
Define and apply the existing or appropriate drop/coalesce policy for events
when a slow client reaches capacity, ensuring backpressure or dropping occurs at
the per-subscriber boundary rather than merely accumulating upstream.

Comment on lines +96 to +103
data = json.dumps(
{
"kind": event.kind,
"id": event.trace_id,
"ts": event.ts,
}
)
yield f"id: {seq}\ndata: {data}\n\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Keep the SSE event schema to kind and id.

Line 100 adds ts, but the endpoint documentation and PR contract specify event data containing only kind and id. This changes the public wire schema.

  • tinyagentos/routes/os_events.py#L96-L103: remove ts from the serialized event data.
  • tests/test_os_events.py#L132-L137: remove the ts assertion.
  • tests/test_os_events.py#L275-L282: remove the ts assertion.
📍 Affects 2 files
  • tinyagentos/routes/os_events.py#L96-L103 (this comment)
  • tests/test_os_events.py#L132-L137
  • tests/test_os_events.py#L275-L282
🤖 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 `@tinyagentos/routes/os_events.py` around lines 96 - 103, Keep the SSE payload
serialized by the event-stream generator limited to kind and id by removing ts
from the data object in tinyagentos/routes/os_events.py lines 96-103. Update the
corresponding assertions in tests/test_os_events.py lines 132-137 and 275-282 to
remove expectations for ts.

@jaylfc

jaylfc commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

nemotron-super review

VERDICT: Found issues

  • desktop/src/hooks/use-os-events.ts:102: Effect missing dependencies ['kinds', 'onEvent']

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

@jaylfc

jaylfc commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Reviewed. This is careful work: cleanup is in a finally, only kind/id/ts cross the wire exactly as the docstring promises, auth fails closed, and the proxy-buffering headers are right. One blocking defect, one design question, and the red CI.

1. BLOCKING — subscriptions and relay tasks leak if the generator is never started

The subscriptions and the two relay tasks are created in the handler body, before gen() exists:

user_q  = await event_bus.subscribe(user_ch)
bcast_q = await event_bus.subscribe("broadcast")
relay_tasks = [asyncio.create_task(_relay(user_q)), asyncio.create_task(_relay(bcast_q))]

but every one of them is torn down inside gen()'s finally. An async generator that is closed without ever being iterated does not execute its body, so that finally never runs. Verified rather than assumed:

never-started generator -> cleanup ran? False
started generator       -> cleanup ran? True

So if the client disconnects between the handler returning and StreamingResponse beginning to stream, or anything else closes the generator before first iteration, you leak two EventBus subscriptions and two never-cancelled tasks, per occurrence. It is not a bounded leak either: the bus keeps those queues in self._queues[channel] forever, so every subsequent published event is copied into queues nobody drains, and the relay tasks keep moving them into a merged queue nobody reads.

Fix: move the subscribe calls and create_task calls inside gen(), immediately inside the try. Then the finally is guaranteed to pair with them. Worth a test that closes the response without consuming it and asserts the bus has no leftover subscribers.

2. Unbounded queues make a slow consumer a memory leak

There are three unbounded queues per connection: user_q and bcast_q from EventBus.subscribe (which does a bare asyncio.Queue()), plus merged here. The relays move events between them as fast as they arrive, with no backpressure and no cap.

A client that stops reading but keeps the socket open, or a stalled proxy, accumulates events without limit. SSE clients doing exactly this is the normal failure mode, not an exotic one.

The bus's unbounded queue is pre-existing and not this PR's fault, but this route is the first long-lived consumer to make it matter, and merged is new here. At minimum give merged a maxsize and decide explicitly what happens when it fills: dropping the oldest and emitting a kind: "events.lagged" frame would let the client resync, which is friendlier than silently blocking the relay. Say in the PR which behaviour you chose, because "queue fills, relay blocks, events silently stop" is the failure a reader will not predict from the code.

3. doc-gate is legitimately red

CI's log is unavailable (0 bytes), so I reproduced locally, running both subcommands as CI does:

invariants -> doc-gate: clean, exit 0
diff-gate  -> DOC-GATE FAIL: routes -- an API route module was added or removed
              exit 1

Correct: tinyagentos/routes/os_events.py is a new route module and docs/agent-coordination.md is untouched. A new user-visible SSE endpoint is exactly what that doc is for, so please document it rather than adding a Docs-Reviewed: trailer.

4. Use a changelog fragment, not CHANGELOG.md

The +6 to CHANGELOG.md predates the changelog.d/ mechanism from #2290 and is the rebase-conflict magnet that change removed. Please move it to changelog.d/2309-<slug>.md.

5. Question: id: {seq} invites a resume the server ignores

Each frame carries id: {seq} where seq is a per-connection counter. In SSE, emitting id: is what makes a browser send Last-Event-ID on reconnect, and the module docstring is explicit that resume is best-effort via the bus replay buffer and not Last-Event-ID. So we advertise a resume capability we do not implement, and the ids are not even stable across connections since seq restarts at 1.

Either drop the id: line, or emit event.trace_id and honour Last-Event-ID. Dropping it is the smaller change and matches the documented behaviour.

Also worth confirming

Every authenticated user subscribes to broadcast, so all of them see kind + trace_id + ts for anything published there. That is activity metadata rather than content, and the no-payload rule limits the blast radius, but I could not establish from the code what actually publishes to broadcast. Could you confirm it is intended to be all-users, and that nothing user-specific reaches it? Asking rather than asserting; I did not find a publisher to check against.


Fix 1 and the two CI items and I am happy. 2 and 5 I would take in the same push, but say so if you disagree on either.

@jaylfc jaylfc added the lead-blocked Lead has blocked this PR; gate_merge.sh refuses at exit 10. label Aug 6, 2026
@jaylfc

jaylfc commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

SUPERSEDED by card tsk-ronapx, closing this PR.

Reason: the branch has been red and unattended since 2026-08-06 (lanes are one-shot - they post [REVIEW] and exit, so the blocking review above could never reach a worker), and dev has moved far enough since that a rebase is a bigger job than a re-cut: changelog.d/ fragments replaced direct CHANGELOG.md edits and doc-gate has been rewritten twice.

The work is not lost. exec/tsk-jmctoa stays on the remote and tsk-ronapx makes merging it STEP 0, then carries every finding from the review above as an explicit demand - the subscription/task leak (with a red-first proof required), the unbounded merged queue, the id: {seq} line, the doc-gate and changelog-fragment items, and the three use-os-events fixes including the vacuous disconnect assertions.

Nothing here needs re-deriving; the replacement PR supersedes this one.

@jaylfc jaylfc closed this Aug 12, 2026
jaylfc added a commit that referenced this pull request Aug 12, 2026
feat(os-events): /api/os/events SSE stream and useOsEvents hook (supersedes #2309)
hognek pushed a commit to hognek/tinyagentos that referenced this pull request Aug 13, 2026
Supersedes PR jaylfc#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lead-blocked Lead has blocked this PR; gate_merge.sh refuses at exit 10.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant