From 71049f9d4e04c0c6259ecd1be4dc7f7dfb15b202 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:36:07 +0000 Subject: [PATCH] fix(cli): Accept null stats on later agent conversation pages The conversation details endpoint returns stats on the first page and stats: null on every page after it. The response schema only allowed a missing stats field, so agent-conversation view failed with "Unexpected response format" on any conversation with more than one page of spans. Co-Authored-By: Ognjen Bostjancic --- packages/cli/src/lib/api/conversations.ts | 2 +- packages/cli/src/types/conversation.ts | 8 +++-- .../cli/test/lib/api/conversations.test.ts | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/lib/api/conversations.ts b/packages/cli/src/lib/api/conversations.ts index cf338228d..3f7f7dc05 100644 --- a/packages/cli/src/lib/api/conversations.ts +++ b/packages/cli/src/lib/api/conversations.ts @@ -159,7 +159,7 @@ export async function getConversationSpans( ); if (page === 0) { - stats = data.stats; + stats = data.stats ?? undefined; title = data.title; } spans.push(...data.spans); diff --git a/packages/cli/src/types/conversation.ts b/packages/cli/src/types/conversation.ts index 87b83649d..1d19ac11d 100644 --- a/packages/cli/src/types/conversation.ts +++ b/packages/cli/src/types/conversation.ts @@ -11,6 +11,7 @@ import { type InferOutput, looseObject, nullable, + nullish, number, object, optional, @@ -120,8 +121,11 @@ export const AgentConversationDetailsSchema = looseObject({ conversationId: string(), title: nullable(string()), spans: array(AgentConversationSpanSchema), - /** Added after the original envelope; optional for older self-hosted servers. */ - stats: optional(ConversationStatsSchema), + /** + * Added after the original envelope; missing on older self-hosted servers. + * The server sends `null` on every page after the first. + */ + stats: nullish(ConversationStatsSchema), }); export type AgentConversationDetails = InferOutput< diff --git a/packages/cli/test/lib/api/conversations.test.ts b/packages/cli/test/lib/api/conversations.test.ts index 0aadb3d07..5512ef7c0 100644 --- a/packages/cli/test/lib/api/conversations.test.ts +++ b/packages/cli/test/lib/api/conversations.test.ts @@ -408,6 +408,35 @@ describe("getConversationSpans", () => { expect(getCapturedUrls()[1]).toContain("cursor=page-2-cursor"); }); + test("keeps first-page stats when later pages send stats: null", async () => { + const stats: ConversationStats = { + errors: 2, + llmCalls: 3, + toolCalls: 4, + toolErrors: 1, + totalCost: 0.5, + totalTokens: 900, + }; + mockSequential([ + { + body: makeEnvelope([makeSpan("span-page-1-aa11")], "Long run", stats), + headers: linkHeader("page-2-cursor"), + }, + { + body: { + ...makeEnvelope([makeSpan("span-page-2-bb22")], null), + stats: null, + }, + }, + ]); + + const result = await getConversationSpans(ORG, CONV_ID); + + expect(result.spans).toHaveLength(2); + expect(result.stats).toEqual(stats); + expect(result.title).toBe("Long run"); + }); + test("returns truncated=true when pagination limit reached", async () => { // Create responses that always have more pages const responses = Array.from({ length: MAX_PAGINATION_PAGES }, (_, i) => ({