{"record":{"id":"7bc98080a589b4e7","repo":"OpenHands/OpenHands","slug":"invalid-conversation-history-response-expected-pa","errorCode":null,"errorMessage":"Invalid conversation history response: expected page.items to be an array.","messagePattern":"Invalid conversation history response: expected page\\.items to be an array\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/hooks/query/use-conversation-history.ts","lineNumber":59,"sourceCode":"    ],\n    enabled: !!conversationId && !!conversation,\n    queryFn: async () => {\n      if (!conversationId) {\n        return { events: [], hasMore: false, nextPageId: null };\n      }\n\n      const page = await EventService.searchEvents(\n        conversationId,\n        conversation?.conversation_url ?? null,\n        conversation?.session_api_key ?? null,\n        {\n          limit: INITIAL_HISTORY_PAGE_SIZE,\n          sortOrder: \"TIMESTAMP_DESC\",\n        },\n      );\n\n      if (!Array.isArray(page.items)) {\n        throw new Error(\n          \"Invalid conversation history response: expected page.items to be an array.\",\n        );\n      }\n\n      // Reverse so callers can append in chronological order.\n      const events = [...page.items].reverse();\n      return {\n        events,\n        hasMore:\n          !!page.next_page_id || page.items.length >= INITIAL_HISTORY_PAGE_SIZE,\n        nextPageId: page.next_page_id ?? null,\n      };\n    },\n    // Keep the cached page so returning to a conversation renders the\n    // last-known discussion instantly (no skeleton). But refetch the tail on\n    // mount so events produced while we were away — e.g. an active /goal loop\n    // that keeps emitting user + agent turns while we're on another\n    // conversation — arrive in one batched REST page instead of being","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/OpenHands/OpenHands/blob/500b4c533e9393e44cb92894bcbb18438ef473b6/src/hooks/query/use-conversation-history.ts#L41-L77","documentation":"Defensive guard inside useConversationHistory's queryFn: after EventService.searchEvents returns a page, the code asserts Array.isArray(page.items) before reversing. The agent-server contract is an EventSearchPage with an items array; a non-array means the response was malformed (wrong shape, null, or a cloud-proxy error envelope). Rather than crashing on .reverse() of undefined, the hook throws an explicit, grep-able error so React Query surfaces a failure.","triggerScenarios":"Initial REST history fetch (sort_order=TIMESTAMP_DESC, limit=50) returns a payload whose `items` field is not an array. Happens when: the cloud proxy returns an error object instead of a page; the agent-server returns a legacy/different shape after an upgrade; a middleware (ingress/proxy) rewrites the body; the response is an empty `null`.","commonSituations":"Agent-server version mismatch (frontend expects the new EventSearchPage shape); cloud proxy returning a 4xx/5xx error envelope that was not unwrapped; ingress returning its own error JSON; custom reverse proxy stripping the items field; response truncated by a body-size limit.","solutions":["Check the actual response body of GET /api/conversations/{id}/events in the browser network tab — confirm `items` exists and is an array.","Verify the agent-server version satisfies compatibility.minimumAgentServer in config/defaults.json (assertAgentServerVersionIsSupported runs on bootstrap, not here, so a bypassed check is possible).","If a custom ingress/proxy is in front of the agent-server, ensure it passes JSON bodies through unmodified.","On the cloud path, confirm callCloudProxy unwraps error envelopes before they reach EventService.searchEvents."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"function isEventSearchPage(value: unknown): value is { items: unknown[]; next_page_id?: string | null } {\n  return typeof value === 'object' && value !== null && Array.isArray((value as any).items);\n}\n// const page = await EventService.searchEvents(...);\n// if (!isEventSearchPage(page)) return { events: [], hasMore: false, nextPageId: null };","typeGuard":"function isValidHistoryPage(page: unknown): page is { items: import('#/types/agent-server/core').OpenHandsEvent[]; next_page_id?: string | null } {\n  return !!page && typeof page === 'object' && Array.isArray((page as { items?: unknown }).items);\n}","tryCatchPattern":null,"preventionTips":["Log the raw response shape when this fires so backend drift is caught early.","Keep the agent-server version pinned to compatibility.minimumAgentServer.","Treat this error as a signal of response-shape drift, not a transient failure — do not blindly retry."],"tags":["events","history","agent-server","defensive"],"backgroundTag":null,"analyzedSha":"500b4c533e9393e44cb92894bcbb18438ef473b6","analyzedAt":"2026-08-12T10:07:46.034Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}