Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
- **PR #2599** by @Michaelyklam (refs #1925) — Add the Slice 4b `RunnerRuntimeAdapter` facade — a protocol-translator client over a future runner/sidecar backend. The facade delegates `start_run`, `observe_run`, `get_run`, and control calls to an injected runner client, normalizes results into the existing `RunStartResult`/`RunEventStream`/`RunStatus`/`ControlResult` dataclasses, carries explicit `profile`/`workspace`/`model` payload fields, and returns bounded `unsupported` control results without owning `AIAgent`, stream lifecycle, cancel/approval/clarify queues, goal state, or cached-agent table. No route wiring, no default-on runner mode, no public response-shape change.
- **PR #2600** by @LumenYoung (refs #2266) — Slimmer WebUI follow-up from the closed LCM/context-engine PR #2266. Adds rendering and persistence for context-engine compression-anchor metadata (when present on a session or live compression event) including an "Indexed context" detail line on auto-compression cards. No agent-layer clone orchestration; WebUI-only metadata surface.



- Add non-sensitive SSE stream runtime diagnostics to deep health checks, including active stream count, subscriber totals, and offline buffered-event counts for stuck or slow WebUI chat investigations.
## [v0.51.93] — 2026-05-19 — Release BQ (stage-386 — 10-PR full sweep batch — RFC Slice 4 runner/sidecar gate + workspace tree toggle width CSS variable + settled file:// markdown link rendering + prompt-cache coverage percentage fix + terminal shell shutdown reap + configured model picker provider preservation + profile-aware assistant display names + state.db reconciliation slice 1 + queued-message cross-session drain fix + stale-stream writeback supersede)

### Fixed
Expand Down
8 changes: 8 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4085,6 +4085,14 @@ def put_nowait(self, item: tuple[str, object]) -> None:
for q in subscribers:
q.put_nowait(item)

def diagnostic_snapshot(self) -> dict[str, int]:
"""Return non-sensitive stream observation counters for health checks."""
with self._lock:
return {
"subscriber_count": len(self._subscribers),
"offline_buffered_events": len(self._offline_buffer),
}


def create_stream_channel() -> StreamChannel:
return StreamChannel()
Expand Down
45 changes: 45 additions & 0 deletions api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3076,6 +3076,47 @@ def _streams_lock_health(timeout_seconds: float = 0.5) -> dict:
STREAMS_LOCK.release()


def _stream_runtime_diagnostics() -> dict:
"""Return non-sensitive SSE stream diagnostics for health/deep status.

The WebUI chat path can feel slow or stuck when streams are alive but no
browser is attached, or when many events are buffering offline. This helper
exposes counts only — stream ids plus subscriber/buffer sizes — and avoids
event payloads, prompts, tool arguments, or paths.
"""
streams = []
total_subscribers = 0
total_offline_buffered_events = 0
with STREAMS_LOCK:
items = list(STREAMS.items())
for stream_id, stream in items:
snapshot = {}
diagnostic_snapshot = getattr(stream, "diagnostic_snapshot", None)
if callable(diagnostic_snapshot):
try:
raw_snapshot = diagnostic_snapshot()
if isinstance(raw_snapshot, dict):
snapshot = raw_snapshot
except Exception:
snapshot = {}
subscriber_count = int(snapshot.get("subscriber_count") or 0)
offline_buffered_events = int(snapshot.get("offline_buffered_events") or 0)
total_subscribers += subscriber_count
total_offline_buffered_events += offline_buffered_events
streams.append({
"stream_id": str(stream_id),
"subscriber_count": subscriber_count,
"offline_buffered_events": offline_buffered_events,
})
streams.sort(key=lambda item: item["stream_id"])
return {
"active_streams": len(streams),
"total_subscribers": total_subscribers,
"total_offline_buffered_events": total_offline_buffered_events,
"streams": streams,
}


def _run_lifecycle_health() -> dict:
"""Return active worker-run state independent of SSE stream presence."""
# Import the module rather than relying only on imported scalar aliases so
Expand Down Expand Up @@ -3124,6 +3165,10 @@ def _deep_health_checks(stream_check: dict | None = None) -> tuple[dict, bool]:
checks: dict[str, dict] = {}

checks["streams_lock"] = stream_check if stream_check is not None else _streams_lock_health()
checks["stream_runtime"] = {
"status": "ok",
**_stream_runtime_diagnostics(),
}
if checks["streams_lock"].get("status") != "ok":
return checks, False

Expand Down
51 changes: 51 additions & 0 deletions tests/test_webui_runtime_diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from api.config import STREAMS, STREAMS_LOCK, create_stream_channel
from api.routes import _stream_runtime_diagnostics


def test_stream_channel_exposes_buffer_and_subscriber_counts():
channel = create_stream_channel()
channel.put_nowait(("token", {"text": "offline"}))

assert channel.diagnostic_snapshot() == {
"subscriber_count": 0,
"offline_buffered_events": 1,
}

subscriber = channel.subscribe()
try:
snapshot = channel.diagnostic_snapshot()
assert snapshot["subscriber_count"] == 1
assert snapshot["offline_buffered_events"] == 1
assert subscriber.get_nowait()[0] == "token"
finally:
channel.unsubscribe(subscriber)


def test_stream_runtime_diagnostics_summarizes_active_stream_channels():
channel = create_stream_channel()
channel.put_nowait(("token", {"text": "offline"}))
subscriber = channel.subscribe()
try:
with STREAMS_LOCK:
previous = dict(STREAMS)
STREAMS.clear()
STREAMS["stream-one"] = channel
try:
payload = _stream_runtime_diagnostics()
finally:
with STREAMS_LOCK:
STREAMS.clear()
STREAMS.update(previous)

assert payload["active_streams"] == 1
assert payload["total_subscribers"] == 1
assert payload["total_offline_buffered_events"] == 1
assert payload["streams"] == [
{
"stream_id": "stream-one",
"subscriber_count": 1,
"offline_buffered_events": 1,
}
]
finally:
channel.unsubscribe(subscriber)
Loading