From b0653997504b30a2d241dfccdb3820c894ec1cd1 Mon Sep 17 00:00:00 2001 From: "devsy-app[bot]" <277138668+devsy-app[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:24:53 +0000 Subject: [PATCH] fix(analytics): page events past API limit cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenHands Cloud conversation-events endpoint caps `limit` at 100 per request. `CloudClient.conversation_events` issued a single call with `limit=500`, which the API rejects with HTTP 422 (`less_than_equal`), so `get()` raised `typer.Exit(1)` and the real-data run produced no output. The `--sample` path was unaffected because it never hits the API. Page through the endpoint in batches of `EVENTS_PAGE_SIZE` (100) using the `page_id` cursor returned as `next_page_id`, stopping at the requested total or when the server signals the end. Verified against the live API: a conversation with 200 events now fetches both pages (limit=200 422s, limit=100 + page_id paginates to completion). This is a pipeline-repair PR, not a failure-mode intervention: it makes the real-data path functional so the pr-gate mechanism can ever produce a real verdict. The post-fix real-data run for yesterday (2026-08-15) shows 14 runs, 0 failed, pr-gate NOT-ACTIONABLE — there is no recurring agent failure mode today; the metric justifying this change is the 100% failure rate of the real-data path itself before the fix. This commit was created by an AI agent as part of an automated daily agent analytics job. --- hack/analytics/analyze_runs.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/hack/analytics/analyze_runs.py b/hack/analytics/analyze_runs.py index b0017add4..4edd8b06f 100644 --- a/hack/analytics/analyze_runs.py +++ b/hack/analytics/analyze_runs.py @@ -66,6 +66,8 @@ app = typer.Typer(add_completion=False, help="Deterministic agent-fleet run analyzer.") +EVENTS_PAGE_SIZE: int = 100 + FEATURE_BUCKETS: list[str] = [ "missing-tool", "test-failure", "cmd-exit-nonzero", "permission-denied", "timeout", "git-baseline", "lint-issue", "commit-signing", @@ -280,9 +282,30 @@ def search_conversations(self, limit: int = 100) -> list[dict[str, Any]]: return normalize_items(data) def conversation_events(self, conv_id: str, limit: int = 500) -> list[dict[str, Any]]: - """Fetch events for one conversation.""" + """Fetch events for one conversation, paging through the API. + + The events endpoint caps ``limit`` at 100 per request, so a single + call with a larger limit is rejected (HTTP 422). Page in batches of + ``EVENTS_PAGE_SIZE`` using the ``page_id`` cursor until ``limit`` is + reached or the server signals the end (``next_page_id`` is empty). + """ path = f"/api/v1/conversation/{conv_id}/events/search" - return normalize_items(self.get(path, {"limit": limit})) + collected: list[dict[str, Any]] = [] + cursor: str | None = None + while len(collected) < limit: + page_size = min(EVENTS_PAGE_SIZE, limit - len(collected)) + params: dict[str, Any] = {"limit": page_size} + if cursor is not None: + params["page_id"] = cursor + data = self.get(path, params) + if not isinstance(data, dict): + collected.extend(normalize_items(data)) + break + collected.extend(normalize_items(data.get("items"))) + cursor = data.get("next_page_id") + if not cursor or len(normalize_items(data.get("items"))) < page_size: + break + return collected def get(self, path: str, params: dict[str, Any]) -> Any: """Issue a GET request and decode JSON, raising on HTTP/parse errors."""