From 75c01c06115062d7ec1b5059421663db550d2b9c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 25 Aug 2026 01:23:54 -0700 Subject: [PATCH] fix(sessions): history pagination skips projected messages or stops advancing (#129099) * fix(sessions): preserve history pagination replay and progress * fix(sessions): avoid shadowing history cursor sequence --- .../tools/sessions-history-tool.test.ts | 49 +++++++++++++++++++ src/agents/tools/sessions-history-tool.ts | 16 +++--- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/agents/tools/sessions-history-tool.test.ts b/src/agents/tools/sessions-history-tool.test.ts index 8b714bd32196..f6bd26ce7b23 100644 --- a/src/agents/tools/sessions-history-tool.test.ts +++ b/src/agents/tools/sessions-history-tool.test.ts @@ -481,6 +481,55 @@ describe("sessions_history redaction", () => { }); }); + it("preserves the Gateway replay cursor for projected siblings from the same row", async () => { + const tool = createSessionsHistoryTool({ + config: {}, + callGateway: async >(): Promise => + ({ + messages: [ + { role: "assistant", content: "projected sibling", __openclaw: { seq: 8 } }, + { role: "assistant", content: "latest", __openclaw: { seq: 9 } }, + ], + offset: 0, + nextOffset: 2, + hasMore: true, + totalMessages: 10, + }) as T, + }); + + const result = await tool.execute("projected-replay", { sessionKey: "main", offset: 0 }); + + expect(result.details).toMatchObject({ + offset: 0, + nextOffset: 2, + hasMore: true, + totalMessages: 10, + }); + }); + + it("keeps history pagination advancing past an already-returned row", async () => { + const tool = createSessionsHistoryTool({ + config: {}, + callGateway: async >(): Promise => + ({ + messages: [{ role: "assistant", content: "visible", __openclaw: { seq: 7 } }], + offset: 4, + nextOffset: 5, + hasMore: true, + totalMessages: 10, + }) as T, + }); + + const result = await tool.execute("cursor-progress", { sessionKey: "main", offset: 4 }); + + expect(result.details).toMatchObject({ + offset: 4, + nextOffset: 5, + hasMore: true, + totalMessages: 10, + }); + }); + it("honors a scoped incarnation grant through the sandbox visibility clamp", async () => { const requesterSessionKey = "agent:main:clickclack:discussion-proof"; const targetSessionKey = "agent:main:main"; diff --git a/src/agents/tools/sessions-history-tool.ts b/src/agents/tools/sessions-history-tool.ts index fc60e78e528e..791fa072e6e7 100644 --- a/src/agents/tools/sessions-history-tool.ts +++ b/src/agents/tools/sessions-history-tool.ts @@ -338,17 +338,15 @@ function resolveSessionsHistoryPaginationMetadata(params: { }; } - // Gateway offsets count newest transcript rows already returned. Recompute - // from the oldest surviving seq after this tool's own filter/cap passes. - const oldestSeq = params.messages + // Respect Gateway replay cursors and this tool's own byte cap while always advancing. + const seq = params.messages .map((message) => readHistoryMessageSeq(message)) - .find((seq): seq is number => typeof seq === "number"); + .find((value): value is number => typeof value === "number"); + const gatewayOffset = result?.nextOffset; const nextOffset = - oldestSeq !== undefined - ? Math.max(offset, totalMessages - oldestSeq + 1) - : typeof result?.nextOffset === "number" - ? result.nextOffset - : undefined; + seq === undefined + ? gatewayOffset + : Math.max(offset + 1, Math.min(gatewayOffset ?? totalMessages, totalMessages - seq + 1)); const hasMore = nextOffset !== undefined ? nextOffset < totalMessages