Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 /<skill> commands as /skill:<name>
Expand Down
2 changes: 1 addition & 1 deletion docs/api-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<messageID>` 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. |
Expand Down
12 changes: 10 additions & 2 deletions src/routes-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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=<cursor> 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":
Expand Down
12 changes: 12 additions & 0 deletions test/protocol-v124-sessions-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading