From b7b76bbe8653c4717f5b1a9c311d76f1e671d0a6 Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Wed, 25 Mar 2026 14:09:46 -0400 Subject: [PATCH 1/2] feat: wire server-side pagination into handleListMessages HTTP endpoint Part of #21550 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/runtime/routes/conversation-routes.ts | 82 +++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/assistant/src/runtime/routes/conversation-routes.ts b/assistant/src/runtime/routes/conversation-routes.ts index e4d308e015f..ab24d420256 100644 --- a/assistant/src/runtime/routes/conversation-routes.ts +++ b/assistant/src/runtime/routes/conversation-routes.ts @@ -48,7 +48,10 @@ import { } from "../../memory/canonical-guardian-store.js"; import { addMessage, + getLastAssistantTimestampBefore, getMessages, + getMessagesPaginated, + type MessageRow, provenanceFromTrustContext, setConversationOriginChannelIfUnset, setConversationOriginInterfaceIfUnset, @@ -360,7 +363,49 @@ export function handleListMessages( if (!resolvedConversationId) { return Response.json({ messages: [] }); } - const rawMessages = getMessages(resolvedConversationId); + + const beforeTimestampRaw = url.searchParams.get("beforeTimestamp"); + const limitRaw = url.searchParams.get("limit"); + + // Validate: reject NaN values with 400 + if (beforeTimestampRaw !== null && isNaN(Number(beforeTimestampRaw))) { + return httpError( + "BAD_REQUEST", + "beforeTimestamp must be a valid number", + 400, + ); + } + if (limitRaw !== null && isNaN(Number(limitRaw))) { + return httpError("BAD_REQUEST", "limit must be a valid number", 400); + } + + const beforeTimestamp = beforeTimestampRaw + ? Number(beforeTimestampRaw) + : undefined; + // Clamp limit to 1-500 range + const limit = limitRaw + ? Math.min(Math.max(Math.floor(Number(limitRaw)), 1), 500) + : undefined; + + // Option A: only paginate when beforeTimestamp is present. + // Initial load and reconnect send limit but no beforeTimestamp — those must continue + // returning all messages for zero regression risk. + const isPaginated = beforeTimestamp != null; + + let rawMessages: MessageRow[]; + let hasMore = false; + + if (isPaginated) { + const result = getMessagesPaginated( + resolvedConversationId, + limit, + beforeTimestamp, + ); + rawMessages = result.messages; + hasMore = result.hasMore; + } else { + rawMessages = getMessages(resolvedConversationId); + } // Parse content blocks and extract text + tool calls const parsed = rawMessages.map((msg) => { @@ -429,6 +474,12 @@ export function handleListMessages( const interfaceFiles = getInterfaceFilesWithMtimes(interfacesDir); let prevAssistantTimestamp = 0; + if (isPaginated && rawMessages.length > 0) { + prevAssistantTimestamp = getLastAssistantTimestampBefore( + resolvedConversationId!, + rawMessages[0].createdAt, + ); + } const messages: RuntimeMessagePayload[] = parsed.map((m) => { let msgAttachments: RuntimeAttachmentMetadata[] = []; if (m.id) { @@ -498,7 +549,17 @@ export function handleListMessages( }; }); - return Response.json({ messages }); + const oldestTimestamp = + rawMessages.length > 0 ? rawMessages[0].createdAt : undefined; + const oldestMessageId = + rawMessages.length > 0 ? rawMessages[0].id : undefined; + + return Response.json({ + messages, + ...(isPaginated ? { hasMore } : {}), + ...(oldestTimestamp != null ? { oldestTimestamp } : {}), + ...(oldestMessageId != null ? { oldestMessageId } : {}), + }); } /** @@ -1502,9 +1563,20 @@ export function conversationRouteDefinitions(deps: { tags: ["messages"], responseBody: z.object({ messages: z.array(z.unknown()).describe("Array of message objects"), - interfaceFiles: z - .array(z.unknown()) - .describe("Interface file paths with modification timestamps"), + hasMore: z + .boolean() + .optional() + .describe("Whether older messages exist beyond this page"), + oldestTimestamp: z + .number() + .optional() + .describe( + "Timestamp of the oldest message in this page (ms since epoch)", + ), + oldestMessageId: z + .string() + .optional() + .describe("ID of the oldest message in this page"), }), handler: ({ url }) => handleListMessages(url, deps.interfacesDir), }, From 56c991bc48847db84d963c7d871457f59c4c3988 Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Wed, 25 Mar 2026 14:16:03 -0400 Subject: [PATCH 2/2] fix: gate pagination fields behind isPaginated and regenerate openapi spec Co-Authored-By: Claude Opus 4.6 (1M context) --- assistant/openapi.yaml | 14 +++++++---- .../src/runtime/routes/conversation-routes.ts | 23 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/assistant/openapi.yaml b/assistant/openapi.yaml index 3c7eb803986..f92c45d11fb 100644 --- a/assistant/openapi.yaml +++ b/assistant/openapi.yaml @@ -4210,13 +4210,17 @@ paths: type: array items: {} description: Array of message objects - interfaceFiles: - type: array - items: {} - description: Interface file paths with modification timestamps + hasMore: + description: Whether older messages exist beyond this page + type: boolean + oldestTimestamp: + description: Timestamp of the oldest message in this page (ms since epoch) + type: number + oldestMessageId: + description: ID of the oldest message in this page + type: string required: - messages - - interfaceFiles additionalProperties: false post: operationId: messages_post diff --git a/assistant/src/runtime/routes/conversation-routes.ts b/assistant/src/runtime/routes/conversation-routes.ts index ab24d420256..2fb05f38d26 100644 --- a/assistant/src/runtime/routes/conversation-routes.ts +++ b/assistant/src/runtime/routes/conversation-routes.ts @@ -549,17 +549,20 @@ export function handleListMessages( }; }); - const oldestTimestamp = - rawMessages.length > 0 ? rawMessages[0].createdAt : undefined; - const oldestMessageId = - rawMessages.length > 0 ? rawMessages[0].id : undefined; + if (isPaginated) { + const oldestTimestamp = + rawMessages.length > 0 ? rawMessages[0].createdAt : undefined; + const oldestMessageId = + rawMessages.length > 0 ? rawMessages[0].id : undefined; + return Response.json({ + messages, + hasMore, + ...(oldestTimestamp != null ? { oldestTimestamp } : {}), + ...(oldestMessageId != null ? { oldestMessageId } : {}), + }); + } - return Response.json({ - messages, - ...(isPaginated ? { hasMore } : {}), - ...(oldestTimestamp != null ? { oldestTimestamp } : {}), - ...(oldestMessageId != null ? { oldestMessageId } : {}), - }); + return Response.json({ messages }); } /**