From 843ef95c3e4fe69a009f40cb9231dfabd6a9f1d5 Mon Sep 17 00:00:00 2001 From: MOLW <85048713+MatrixAdventurer@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:12:32 +0800 Subject: [PATCH 1/3] fix(openclaw): follow snapshot discovery pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 由 LW 授权的 ChatGPT Personal 智能体提交。 --- plugins/openclaw/openclaw/http-client.ts | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/plugins/openclaw/openclaw/http-client.ts b/plugins/openclaw/openclaw/http-client.ts index 80c3f106..be3b7a5d 100644 --- a/plugins/openclaw/openclaw/http-client.ts +++ b/plugins/openclaw/openclaw/http-client.ts @@ -291,10 +291,29 @@ export class MemoriaHttpTransport { private async listSnapshots(args: Record) { const limit = typeof args.limit === "number" ? args.limit : 20; - const offset = typeof args.offset === "number" ? args.offset : 0; - const data = await this.get(`/v1/snapshots?limit=${limit}&offset=${offset}`); - // Format as MCP-compatible text so parseSnapshotList() in client.ts works - return this.formatSnapshotList(data); + let offset = typeof args.offset === "number" ? args.offset : 0; + // Explicit transport-level pagination remains a single page. The public + // client/tool supplies neither argument and promises all known snapshots. + if (typeof args.limit === "number" || typeof args.offset === "number") { + return this.formatSnapshotList(await this.get(`/v1/snapshots?limit=${limit}&offset=${offset}`)); + } + const snapshots: unknown[] = []; + for (let page = 0; page < this.maxListPages; page++) { + const data = await this.get(`/v1/snapshots?limit=${limit}&offset=${offset}`); + const rec = this.asRecord(data); + if (!Array.isArray(rec?.snapshots)) { + // Preserve older unpaginated response formats on the first request. + if (page === 0) return this.formatSnapshotList(data); + throw new Error("Memoria snapshot listing returned a malformed page"); + } + const items = rec.snapshots; + snapshots.push(...items); + if (rec.has_more !== true) return this.formatSnapshotList({ snapshots }); + if (items.length === 0) throw new Error("Memoria snapshot listing returned an empty page with has_more"); + offset += items.length; + } + // Do not let listSnapshots/stats present a bounded prefix as a complete list. + throw new Error(`Memoria snapshot listing exceeded maxListPages (${this.maxListPages})`); } private async rollbackSnapshot(args: Record) { From 3c075b4dcde8e050476f05eac6df8c8d5169c94e Mon Sep 17 00:00:00 2001 From: MOLW <85048713+MatrixAdventurer@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:12:36 +0800 Subject: [PATCH 2/3] test(openclaw): cover complete and failed snapshot scans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 由 LW 授权的 ChatGPT Personal 智能体提交。 --- .../__tests__/snapshot-pagination.test.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 plugins/openclaw/openclaw/__tests__/snapshot-pagination.test.ts diff --git a/plugins/openclaw/openclaw/__tests__/snapshot-pagination.test.ts b/plugins/openclaw/openclaw/__tests__/snapshot-pagination.test.ts new file mode 100644 index 00000000..97b872af --- /dev/null +++ b/plugins/openclaw/openclaw/__tests__/snapshot-pagination.test.ts @@ -0,0 +1,116 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterEach, describe, expect, it } from "vitest"; +import { MemoriaClient } from "../client.js"; +import { MemoriaHttpTransport } from "../http-client.js"; +import { buildApiConfig } from "./helpers.js"; + +const servers: Server[] = []; + +async function serveSnapshots(total: number, mode = "normal") { + const offsets: number[] = []; + const snapshots = Array.from({ length: total }, (_, i) => ({ + name: `release_${i}`, snapshot_name: `mem_snap_release_${i}`, + timestamp: "2026-10-21T00:00:00Z", + })); + const server = createServer((request, response) => { + const url = new URL(request.url!, "http://localhost"); + response.setHeader("Content-Type", "application/json"); + if (url.pathname === "/v1/memories") { + response.end(JSON.stringify({ items: [], next_cursor: null })); + return; + } + if (url.pathname === "/v1/branches") { + response.end(JSON.stringify({ branches: [{ name: "main", active: true }] })); + return; + } + const offset = Number(url.searchParams.get("offset") ?? 0); + const limit = Number(url.searchParams.get("limit") ?? 20); + offsets.push(offset); + if (mode === "fail-later" && offset > 0) { + response.writeHead(503); + response.end("unavailable"); + return; + } + const items = mode === "empty-more" ? [] : snapshots.slice(offset, offset + limit); + response.end(JSON.stringify({ + snapshots: items, total, offset, limit, + has_more: mode === "empty-more" || offset + items.length < total, + })); + }); + servers.push(server); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + return { + apiUrl: `http://127.0.0.1:${(server.address() as AddressInfo).port}`, + offsets, snapshots, + }; +} + +afterEach(async () => { + await Promise.all(servers.splice(0).map(async (server) => { + server.closeAllConnections(); + await new Promise((resolve, reject) => { + server.close((error) => error ? reject(error) : resolve()); + }); + })); +}); + +describe("snapshot discovery pagination (real HTTP)", () => { + it("lists snapshots beyond the first 20 and counts them in stats", async () => { + const fixture = await serveSnapshots(45); + const client = new MemoriaClient(buildApiConfig({ apiUrl: fixture.apiUrl })); + try { + expect((await client.listSnapshots("u")).map((s) => s.name)) + .toEqual(fixture.snapshots.map((s) => s.name)); + expect(fixture.offsets).toEqual([0, 20, 40]); + expect((await client.stats("u")).snapshotCount).toBe(45); + } finally { + client.close(); + } + }); + + it("rejects a truncated scan rather than returning a false complete count", async () => { + const fixture = await serveSnapshots(21); + const client = new MemoriaClient(buildApiConfig({ apiUrl: fixture.apiUrl, maxListPages: 1 })); + try { + await expect(client.listSnapshots("u")).rejects.toThrow(/maxListPages/); + const stats = await client.stats("u"); + expect(stats.snapshotCount).toBeNull(); + expect(stats.limitations?.join(" ")).toMatch(/Snapshot statistics unavailable/); + expect(fixture.offsets).toEqual([0, 0]); + } finally { + client.close(); + } + }); + + it("fails without looping when an empty page claims more snapshots", async () => { + const fixture = await serveSnapshots(21, "empty-more"); + const client = new MemoriaClient(buildApiConfig({ apiUrl: fixture.apiUrl })); + try { + await expect(client.listSnapshots("u")).rejects.toThrow(/empty.*page/i); + expect(fixture.offsets).toEqual([0]); + } finally { + client.close(); + } + }); + + it("propagates a later-page HTTP error instead of returning the first page", async () => { + const fixture = await serveSnapshots(21, "fail-later"); + const client = new MemoriaClient(buildApiConfig({ apiUrl: fixture.apiUrl })); + try { + await expect(client.listSnapshots("u")).rejects.toThrow(/503/); + expect(fixture.offsets).toEqual([0, 20]); + } finally { + client.close(); + } + }); + + it("retains explicit low-level page selection", async () => { + const fixture = await serveSnapshots(45); + const transport = new MemoriaHttpTransport(buildApiConfig({ apiUrl: fixture.apiUrl }), "u"); + const result = await transport.callTool("memory_snapshots", { limit: 2, offset: 20 }); + expect(JSON.stringify(result)).toContain("release_20"); + expect(JSON.stringify(result)).not.toContain("release_22"); + expect(fixture.offsets).toEqual([20]); + }); +}); From ad2096906ecff33f53bf75d066f60471754ce3e3 Mon Sep 17 00:00:00 2001 From: MOLW <85048713+MatrixAdventurer@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:12:42 +0800 Subject: [PATCH 3/3] docs(openclaw): describe bounded snapshot discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 由 LW 授权的 ChatGPT Personal 智能体提交。 --- plugins/openclaw/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/openclaw/README.md b/plugins/openclaw/README.md index 1417b985..1a333c9e 100644 --- a/plugins/openclaw/README.md +++ b/plugins/openclaw/README.md @@ -163,6 +163,9 @@ The plugin exposes these tools to OpenClaw: - `memory_branch`, `memory_branches`, `memory_checkout`, `memory_branch_delete`, `memory_merge`, `memory_diff` - Compatibility CLI aliases under `openclaw ltm ...` +In API mode, snapshot discovery follows `has_more` pages up to `maxListPages`. +If that bound is reached or pagination fails, listing reports an error and snapshot statistics remain unavailable rather than reporting a partial count as complete. + ## Common Pitfalls **`openclaw memoria` vs `openclaw memory`:** This plugin uses `openclaw memoria`. The `openclaw memory` namespace is OpenClaw's built-in file memory — a different system entirely.