perf(sessions): push new-session detection over SSE #181

Merged
dries merged 1 commit from feat/push-new-sessions into main 2026-06-29 23:14:31 +02:00
Owner

Problem

New sessions only appeared on the next list-poll tick — ~9s worst case (4s background refresher + 3–5s frontend poll) — because session creation was never broadcast. Detection relied entirely on polling OpenCode's SQLite DB.

Approach

Reuse infrastructure that already exists:

  • The auto-approve watcher already holds a long-lived connection to every running OpenCode instance's /event SSE stream.
  • The /api/events broadcast hub and the frontend useGlobalEvents listener already exist.

On the first sighting of a session.updated event for a given session ID, the watcher invalidates the sessions cache and broadcasts a new ocman.session.changed event. The frontend invalidates the ['sessions'] query, so the list refetches immediately instead of waiting for the next poll.

A seen-set dedupes session.updated (which fires per turn/token) so only the first sighting — i.e. "a session appeared" — triggers work. Per-keystroke updates are dropped.

Result

  • New live session: ~9s → sub-second.
  • More decoupled from the DB, not less: the trigger is OpenCode's own event; the DB is read once on demand instead of every 4s. The 4s refresher stays as the fallback for dead/never-running instances.

Changes

Backend

  • autoapprove.gosession.updated dispatch case + dispatchSessionChangedonSessionChanged tee callback.
  • autoapprove_watcher.gohandleSessionChanged with seen-set dedup; busts cache + broadcasts on first sight.
  • broadcast.gobroadcastSessionChangedocman.session.changed.
  • models_cache.goInvalidateSessionsCache() (expires entries, keeps last-good for stale-on-busy).

Frontend

  • useGlobalEvents.tsocman.session.changed listener + onSessionChanged registry.
  • App.tsx — invalidates ['sessions'] on the event.

Tests

  • Tee dispatch of session.updated (envelope/flat/both casings/missing-id).
  • Watcher dedup (first sight acts, duplicates no-op, empty id ignored).
  • InvalidateSessionsCache forces a refetch.
  • Frontend listener register/notify/unsubscribe.

All Go + frontend suites, go vet, tsc, ESLint, and the platform-branching check pass.

Known limitations

  • Push only covers currently-running OpenCode instances (lsof-discovered). Dead/never-running instances still surface via the existing poll.
  • Seen-set is unbounded (marked with a ponytail: comment); swap to LRU if per-machine session count ever grows large.
## Problem New sessions only appeared on the next list-poll tick — ~9s worst case (4s background refresher + 3–5s frontend poll) — because session creation was never broadcast. Detection relied entirely on polling OpenCode's SQLite DB. ## Approach Reuse infrastructure that already exists: - The auto-approve watcher already holds a long-lived connection to every running OpenCode instance's `/event` SSE stream. - The `/api/events` broadcast hub and the frontend `useGlobalEvents` listener already exist. On the **first** sighting of a `session.updated` event for a given session ID, the watcher invalidates the sessions cache and broadcasts a new `ocman.session.changed` event. The frontend invalidates the `['sessions']` query, so the list refetches immediately instead of waiting for the next poll. A **seen-set** dedupes `session.updated` (which fires per turn/token) so only the first sighting — i.e. "a session appeared" — triggers work. Per-keystroke updates are dropped. ## Result - New live session: **~9s → sub-second**. - More decoupled from the DB, not less: the trigger is OpenCode's own event; the DB is read once on demand instead of every 4s. The 4s refresher stays as the fallback for dead/never-running instances. ## Changes **Backend** - `autoapprove.go` — `session.updated` dispatch case + `dispatchSessionChanged` → `onSessionChanged` tee callback. - `autoapprove_watcher.go` — `handleSessionChanged` with seen-set dedup; busts cache + broadcasts on first sight. - `broadcast.go` — `broadcastSessionChanged` → `ocman.session.changed`. - `models_cache.go` — `InvalidateSessionsCache()` (expires entries, keeps last-good for stale-on-busy). **Frontend** - `useGlobalEvents.ts` — `ocman.session.changed` listener + `onSessionChanged` registry. - `App.tsx` — invalidates `['sessions']` on the event. ## Tests - Tee dispatch of `session.updated` (envelope/flat/both casings/missing-id). - Watcher dedup (first sight acts, duplicates no-op, empty id ignored). - `InvalidateSessionsCache` forces a refetch. - Frontend listener register/notify/unsubscribe. All Go + frontend suites, `go vet`, `tsc`, ESLint, and the platform-branching check pass. ## Known limitations - Push only covers currently-running OpenCode instances (lsof-discovered). Dead/never-running instances still surface via the existing poll. - Seen-set is unbounded (marked with a `ponytail:` comment); swap to LRU if per-machine session count ever grows large.
perf(sessions): push new-session detection over SSE
All checks were successful
CI / Frontend (pull_request) Successful in 4m13s
CI / Backend (pull_request) Successful in 5m2s
CI / Playwright E2E (pull_request) Successful in 6m33s
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m34s
CI / Build (pull_request) Successful in 3m18s
CI / Semantic Tag (pull_request) Has been skipped
CI / Coverage Ratchet (pull_request) Successful in 4m27s
57e3c368d1
New sessions appeared only on the next list-poll tick (~9s worst case)
because creation was never broadcast. Tap the OpenCode /event stream
the auto-approve watcher already consumes: on the first sighting of a
session.updated event for a session ID, invalidate the sessions cache
and broadcast ocman.session.changed so clients refetch immediately.

A seen-set dedupes per-turn/per-token session.updated events so only
the first sighting (session created) triggers a refresh. Live sessions
now surface sub-second while the background refresher remains the
fallback for non-live instances.

Coverage ratchet: pass

Suite Baseline This PR Δ
go 69.40% 69.50% +0.10
frontend 57.39% 57.41% +0.02

Tolerance: -0.1%. Baseline stored on gh-pages.

<!-- coverage-ratchet --> ### Coverage ratchet: ✅ pass | Suite | Baseline | This PR | Δ | | |---|---|---|---|---| | go | 69.40% | 69.50% | +0.10 | ✅ | | frontend | 57.39% | 57.41% | +0.02 | ✅ | _Tolerance: -0.1%. Baseline stored on `gh-pages`._
dries merged commit 6ca9e723f9 into main 2026-06-29 23:14:31 +02:00
Sign in to join this conversation.
No description provided.