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
This commit is contained in:
Peter Steinberger
2026-08-25 01:23:54 -07:00
committed by GitHub
parent cb7a1a76df
commit 75c01c0611
2 changed files with 56 additions and 9 deletions
@@ -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 <T = Record<string, unknown>>(): Promise<T> =>
({
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 <T = Record<string, unknown>>(): Promise<T> =>
({
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";
+7 -9
View File
@@ -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