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) => ({