diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc5aa3..be71245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project uses [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed + +- Long sessions no longer open blank in OpenChamber. The v1 message list now + honors `before` and returns an `x-next-cursor` header, so the UI pages older + history instead of stopping at the newest 50 messages, which could all be + assistant replies with no user message to anchor them. + ## [0.2.8] - 2026-10-15 - fix(session): resend bare / commands as /skill: diff --git a/docs/api-surface.md b/docs/api-surface.md index f9f1154..14caeb7 100644 --- a/docs/api-surface.md +++ b/docs/api-surface.md @@ -102,7 +102,7 @@ invented resource IDs. | GET | `/session/{sessionID}` | 200 | Session record; 404 if unknown. | | PATCH | `/session/{sessionID}` | 200 | Updated session record; supports title, archive time, and metadata. | | DELETE | `/session/{sessionID}` | 200 | Session removal result. | -| GET | `/session/{sessionID}/message` | 200 | Message list; optional `limit` returns the newest bounded page. | +| GET | `/session/{sessionID}/message` | 200 | Message list; optional `limit` returns the newest bounded page and `before=` pages older history. An `x-next-cursor` header (the page's oldest message ID) is set while older messages remain. | | GET | `/session/{sessionID}/message/{messageID}` | 200 | One message; 404 if unknown. | | GET | `/session/{sessionID}/children` | 200 | Direct child sessions. | | GET | `/session/{sessionID}/todo` | 200 | Session todo items. | diff --git a/src/routes-v1.ts b/src/routes-v1.ts index 8b69c20..eaa8628 100644 --- a/src/routes-v1.ts +++ b/src/routes-v1.ts @@ -180,9 +180,17 @@ export async function handleV1( json(res, 200, await services.sessions.remove(sessionId)); return; case "GET /message": { - const messages = services.sessions.messages(sessionId) ?? []; + let messages = services.sessions.messages(sessionId) ?? []; + const before = url.searchParams.get("before"); + if (before) { + const index = messages.findIndex((message) => message.info.id === before); + if (index !== -1) messages = messages.slice(0, index); + } const limit = numberParam(url.searchParams.get("limit")); - json(res, 200, limit === undefined ? messages : limit === 0 ? [] : messages.slice(-limit)); + const page = limit === undefined ? messages : limit === 0 ? [] : messages.slice(-limit); + // The UI pages older history with ?before= until no cursor comes back. + if (page.length > 0 && page.length < messages.length) res.setHeader("x-next-cursor", page[0]!.info.id); + json(res, 200, page); return; } case "GET /children": diff --git a/test/protocol-v124-sessions-history.test.ts b/test/protocol-v124-sessions-history.test.ts index d7fb60a..00c216f 100644 --- a/test/protocol-v124-sessions-history.test.ts +++ b/test/protocol-v124-sessions-history.test.ts @@ -184,6 +184,18 @@ test("V124-006: honors message list limit", { timeout: 10_000 }, async (t) => { assert.equal(messages.length, 1); assert.ok(isRecord(messages[0])); assert.equal(messages[0].info && isRecord(messages[0].info) ? messages[0].info.id : undefined, "msg-two"); + const cursor = response.headers.get("x-next-cursor"); + assert.equal(cursor, "msg-two"); + + const older = await fetch( + `${url}/session/${sessionID}/message?directory=${encodeURIComponent(directory)}&limit=1&before=${cursor}`, + ); + const olderMessages: unknown = await older.json(); + if (!Array.isArray(olderMessages)) throw new TypeError("expected a message array"); + assert.equal(olderMessages.length, 1); + assert.ok(isRecord(olderMessages[0]) && isRecord(olderMessages[0].info)); + assert.equal(olderMessages[0].info.id, "msg-one"); + assert.equal(older.headers.get("x-next-cursor"), null); }); test("V124-007: returns a v1 message by id", { timeout: 10_000 }, async (t) => {