@jskit-ai/assistant-runtime 0.1.215 → 0.1.216

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -122,6 +122,7 @@ export default defineConfig({
122
122
  if (denied || !state) return json({ error: "Access denied.", code: "conversation_forbidden" }, 403);
123
123
  if (request.method === "GET" && !match[2]) {
124
124
  return json(pageLimits.has(id) ? { ...state, ...await transcript.readConversationLogPage(id, {
125
+ pageUnit: "messages",
125
126
  beforeTurnId: url.searchParams.get("beforeTurnId") || "",
126
127
  limit: url.searchParams.get("limit") || pageLimits.get(id)
127
128
  }) } : state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.215",
3
+ "version": "0.1.216",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -12,7 +12,7 @@
12
12
  "./server": "./src/server/index.js"
13
13
  },
14
14
  "dependencies": {
15
- "@jskit-ai/assistant-core": "0.1.222",
15
+ "@jskit-ai/assistant-core": "0.1.223",
16
16
  "json-rest-schema": "^1.0.17"
17
17
  },
18
18
  "peerDependencies": {
@@ -274,7 +274,9 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
274
274
  // refresh in this same read, rather than dropping that newly loaded page.
275
275
  while (valid()) {
276
276
  const revision = historyRevision;
277
- const oldest = olderPages.value[0]?.conversationLog?.[0]?.turnId;
277
+ const oldestTurn = olderPages.value[0]?.conversationLog?.[0];
278
+ const oldest = oldestTurn?.turnId;
279
+ const oldestCursor = olderPages.value[0]?.pagination?.nextBeforeTurnId || oldest;
278
280
  const latest = await api.readConversation(identity.conversationId);
279
281
  if (!valid()) return latest;
280
282
  const pages = [];
@@ -282,7 +284,9 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
282
284
  const cursors = new Set();
283
285
  while (oldest && page.pagination.hasMoreBefore) {
284
286
  const cursor = page.pagination.nextBeforeTurnId || page.pagination.oldestTurnId;
285
- if (cursor && compareTurnIds(cursor, oldest) <= 0) break;
287
+ const reachedFullOldestTurn = page.pagination.oldestTurnId === oldest && !page.conversationLog[0]?.paginationPartial;
288
+ const passedOldestTurn = compareTurnIds(page.pagination.oldestTurnId, oldest) < 0;
289
+ if (cursor && (cursor === oldestCursor || passedOldestTurn || reachedFullOldestTurn)) break;
286
290
  if (!cursor || cursors.has(cursor)) throw new Error("Older conversation history could not be refreshed. Reload chat to try again.");
287
291
  cursors.add(cursor);
288
292
  page = normalizeConversationLogPage(await api.readConversation(identity.conversationId, {
@@ -291,7 +295,14 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
291
295
  if (!valid()) return latest;
292
296
  // A removed oldest row can make this page reach farther back than the
293
297
  // reader loaded. Keep the original visible boundary and page metadata.
294
- const saved = page.conversationLog.filter(turn => compareTurnIds(turn.turnId, oldest) >= 0);
298
+ const saved = page.conversationLog.filter(turn => compareTurnIds(turn.turnId, oldest) >= 0).map(turn => {
299
+ if (turn.turnId !== oldest || !oldestTurn.paginationPartial) return turn;
300
+ const roles = ["system", "user", "assistant"];
301
+ const excluded = roles.slice(0, roles.findIndex(role => oldestTurn[role]));
302
+ return { ...turn, paginationPartial: true,
303
+ ...Object.fromEntries(excluded.map(role => [role, null])),
304
+ messages: turn.messages?.filter(message => !excluded.includes(message.role)) };
305
+ });
295
306
  pages.unshift(normalizeConversationLogPage({ ...page, conversationLog: saved, pagination: {
296
307
  ...page.pagination, count: saved.length, oldestTurnId: saved[0]?.turnId || "",
297
308
  hasMoreBefore: page.pagination.hasMoreBefore || saved.length < page.conversationLog.length
@@ -501,7 +512,8 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
501
512
  const finish = (result) => {
502
513
  if (typeof complete === "function") complete(result);
503
514
  };
504
- const beforeTurnId = normalizeConversationLogPagination(oldestLoadedPage.value?.pagination).oldestTurnId ||
515
+ const pagination = normalizeConversationLogPagination(oldestLoadedPage.value?.pagination);
516
+ const beforeTurnId = pagination.nextBeforeTurnId || pagination.oldestTurnId ||
505
517
  String(turns.value[0]?.turnId || "").trim();
506
518
  if (!available.value || !hasMoreBefore.value || loadingMore.value || !beforeTurnId) {
507
519
  finish({ changed: false, loaded: false });
@@ -519,10 +531,13 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
519
531
  return false;
520
532
  }
521
533
  const previousOldestTurnId = String(turns.value[0]?.turnId || "").trim();
534
+ const previousMessageCount = turns.value.reduce((count, turn) => count + (turn.messages?.length || 0), 0);
522
535
  historyRevision += 1;
523
536
  olderPages.value = [normalizeConversationLogPage(page), ...olderPages.value];
524
537
  const nextOldestTurnId = String(turns.value[0]?.turnId || "").trim();
525
- const changed = Boolean(nextOldestTurnId && nextOldestTurnId !== previousOldestTurnId);
538
+ const nextMessageCount = turns.value.reduce((count, turn) => count + (turn.messages?.length || 0), 0);
539
+ const changed = Boolean(nextOldestTurnId && (nextOldestTurnId !== previousOldestTurnId ||
540
+ nextMessageCount !== previousMessageCount));
526
541
  finish({ changed, loaded: true });
527
542
  return true;
528
543
  } catch (failure) {
@@ -1628,7 +1628,7 @@ test("paged history uses the original page reader and keeps older messages throu
1628
1628
  user: { role: "user", messageId: `history-${index + 1}`, text: `Saved question ${index + 1}` },
1629
1629
  ...(index < 4 ? { assistant: { role: "assistant", messageId: `answer-${index + 1}`, text: `Saved answer ${index + 1}` } } : {})
1630
1630
  }));
1631
- await f.command("history", { id: "chat:1", turns, limit: 2 });
1631
+ await f.command("history", { id: "chat:1", turns, limit: 4 });
1632
1632
  await f.notify("chat:1");
1633
1633
  await expect(f.primary.getByText("Saved question 5", { exact: true })).toHaveCount(1);
1634
1634
  await expect(f.primary.getByText("Saved question 1", { exact: true })).toHaveCount(0);
@@ -1639,7 +1639,7 @@ test("paged history uses the original page reader and keeps older messages throu
1639
1639
  assert.deepEqual(await f.page.evaluate(() => window.historyCompletion), { changed: true, loaded: true });
1640
1640
  await expect(f.primary.getByText("Saved question 2", { exact: true })).toHaveCount(1);
1641
1641
  const paged = (await f.state()).requests.filter(request => request.query.beforeTurnId);
1642
- assert.deepEqual(paged.map(request => request.query), [{ beforeTurnId: "000004", limit: "2" }]);
1642
+ assert.deepEqual(paged.map(request => request.query), [{ beforeTurnId: 'message:["000003","assistant"]', limit: "4" }]);
1643
1643
  await f.page.evaluate(() => window.conversationFixture.socket.notify("chat:1", {
1644
1644
  type: "message", turnId: "000005", messageId: "live-answer", role: "assistant", status: "inProgress", text: "Live answer",
1645
1645
  streaming: { revision: 1, messages: [{ turnId: "000005", messageId: "live-answer", role: "assistant", status: "inProgress", text: "Live answer" }] }
@@ -2230,7 +2230,7 @@ test("canonical incoming messages preserve paged history and the scrolled-up rea
2230
2230
  user: { messageId: `loaded-user-${index}`, role: "user", text: `Loaded question ${index + 1}. ` + "Keep the earlier discussion. ".repeat(6) },
2231
2231
  assistant: { messageId: `loaded-answer-${index}`, role: "assistant", text: `Loaded answer ${index + 1}. ` + "Keep the earlier answer. ".repeat(6) }
2232
2232
  }));
2233
- await f.command("history", { id: "chat:1", turns, limit: 6 });
2233
+ await f.command("history", { id: "chat:1", turns, limit: 12 });
2234
2234
  await f.notify("chat:1");
2235
2235
  await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().turns.value.length)).toBe(6);
2236
2236
  assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
@@ -2246,7 +2246,7 @@ test("canonical incoming messages preserve paged history and the scrolled-up rea
2246
2246
  await f.input.fill("Keep this unsent draft");
2247
2247
  await f.input.evaluate(element => { element.focus({ preventScroll: true }); element.setSelectionRange(2, 7); });
2248
2248
  turns.push({ turnId: "000025", user: { messageId: "incoming-user", role: "user", text: "New incoming question" } });
2249
- await f.command("history", { id: "chat:1", turns, limit: 6 });
2249
+ await f.command("history", { id: "chat:1", turns, limit: 12 });
2250
2250
  await f.notify("chat:1");
2251
2251
  await expect(f.primary.getByText("New incoming question", { exact: true })).toHaveCount(1);
2252
2252
  const ids = await f.page.evaluate(() => window.conversationFixture.current().turns.value.map(turn => turn.turnId));
@@ -2267,7 +2267,7 @@ for (const width of [390, 768, 1280]) {
2267
2267
  user: { role: "user", messageId: `atomic-user-${index}`, text: `Atomic saved question ${index + 1}. ` + "Retain visible discussion. ".repeat(8) },
2268
2268
  assistant: { role: "assistant", messageId: `atomic-answer-${index}`, text: `Atomic saved answer ${index + 1}. ` + "Retain visible answer. ".repeat(8) }
2269
2269
  }));
2270
- await f.command("history", { id: "chat:1", turns, limit: 6 });
2270
+ await f.command("history", { id: "chat:1", turns, limit: 12 });
2271
2271
  await f.notify("chat:1");
2272
2272
  await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().turns.value.length)).toBe(6);
2273
2273
  assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
@@ -2284,7 +2284,7 @@ for (const width of [390, 768, 1280]) {
2284
2284
  const fresh = turns.filter(turn => !["000007", "000010"].includes(turn.turnId));
2285
2285
  fresh[8] = { ...fresh[8], assistant: { ...fresh[8].assistant, text: "Authoritatively replaced saved answer" } };
2286
2286
  fresh.push({ turnId: "000025", user: { role: "user", messageId: "atomic-incoming", text: "Atomic new question" } });
2287
- await f.command("history", { id: "chat:1", turns: fresh, limit: 6 });
2287
+ await f.command("history", { id: "chat:1", turns: fresh, limit: 12 });
2288
2288
  let release;
2289
2289
  let held = false;
2290
2290
  const reads = "**/api/assistant/home/conversations/chat%3A1?**";
@@ -2971,3 +2971,43 @@ test("unknown delivery reports its current result and allows different idle inpu
2971
2971
  assert.equal(await f.page.evaluate(id => window.conversationFixture.current().delivery.find(id).status,
2972
2972
  old.input.messageId), "uncertain");
2973
2973
  });
2974
+
2975
+
2976
+ for (const width of [1280, 390]) test(`ten real messages per history load across split turns at ${width}px`, options, async t => {
2977
+ const f = await fixture(t, width);
2978
+ const turns = [];
2979
+ let id = 0;
2980
+ for (let index = 1; index <= 16; index++) {
2981
+ const user = { role: "user", messageId: `ten-user-${index}`, text: `Ten question ${index}` };
2982
+ const assistant = index < 16 ? { role: "assistant", messageId: `ten-answer-${index}`, text: `Ten answer ${index}` } : null;
2983
+ turns.push({ turnId: String(++id).padStart(6, "0"), user, assistant, messages: [user, assistant].filter(Boolean) });
2984
+ for (let reasoning = 0; reasoning < 4; reasoning++) {
2985
+ const thought = { role: "thinking", messageId: `ten-thought-${index}-${reasoning}`, text: `Reasoning ${index} ${reasoning}` };
2986
+ turns.push({ turnId: String(++id).padStart(6, "0"), thinking: [thought], messages: [thought] });
2987
+ }
2988
+ }
2989
+ await f.command("history", { id: "chat:1", turns, limit: 10 });
2990
+ await f.notify("chat:1");
2991
+ const messageCount = () => f.page.evaluate(() => window.conversationFixture.current().turns.value
2992
+ .reduce((count, turn) => count + [turn.system, turn.user, turn.assistant].filter(Boolean).length, 0));
2993
+ await expect.poll(messageCount).toBe(10);
2994
+ await expect(f.primary.getByText("Ten question 11", { exact: true })).toHaveCount(0);
2995
+ await expect(f.primary.getByText("Ten answer 11", { exact: true })).toHaveCount(1);
2996
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
2997
+ await expect.poll(messageCount).toBe(20);
2998
+ await f.page.evaluate(() => { window.beforeTenMessageRefresh = window.conversationFixture.current().snapshot.value; });
2999
+ await f.notify("chat:1");
3000
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().snapshot.value !== window.beforeTenMessageRefresh)).toBe(true);
3001
+ await expect.poll(messageCount).toBe(20);
3002
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
3003
+ await expect.poll(messageCount).toBe(30);
3004
+ await expect(f.primary.getByText("Ten question 1", { exact: true })).toHaveCount(0);
3005
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore({
3006
+ complete: result => { window.tenMessageCompletion = result; }
3007
+ })), true);
3008
+ await expect.poll(messageCount).toBe(31);
3009
+ assert.deepEqual(await f.page.evaluate(() => window.tenMessageCompletion), { changed: true, loaded: true });
3010
+ await expect(f.primary.getByText("Ten question 1", { exact: true })).toHaveCount(1);
3011
+ await expect(f.primary.getByRole("button", { name: "Load older messages", exact: true })).toHaveCount(0);
3012
+ assert.deepEqual(await f.sent(), []);
3013
+ });