@jskit-ai/assistant-runtime 0.1.211 → 0.1.213

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.211",
3
+ "version": "0.1.213",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -12,19 +12,19 @@
12
12
  "./server": "./src/server/index.js"
13
13
  },
14
14
  "dependencies": {
15
- "@jskit-ai/assistant-core": "0.1.218",
15
+ "@jskit-ai/assistant-core": "0.1.220",
16
16
  "json-rest-schema": "^1.0.17"
17
17
  },
18
18
  "peerDependencies": {
19
- "@jskit-ai/http-runtime": "0.1.238",
20
- "@jskit-ai/http-web": "0.1.85",
21
- "@jskit-ai/kernel": "0.1.240",
22
- "@jskit-ai/realtime": "0.1.237",
23
- "@jskit-ai/shell-web": "0.1.244",
19
+ "@jskit-ai/http-runtime": "0.1.240",
20
+ "@jskit-ai/http-web": "0.1.87",
21
+ "@jskit-ai/kernel": "0.1.242",
22
+ "@jskit-ai/realtime": "0.1.239",
23
+ "@jskit-ai/shell-web": "0.1.246",
24
24
  "@tanstack/vue-query": "^5.90.5",
25
25
  "vue": "^3.5.13",
26
26
  "vuetify": "^4.0.0",
27
- "@jskit-ai/database-runtime": "0.1.240"
27
+ "@jskit-ai/database-runtime": "0.1.242"
28
28
  },
29
29
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
30
30
  "jskit": {
@@ -148,6 +148,10 @@ function subscribeAssistantConversation({
148
148
  changedPresentation = true;
149
149
  }
150
150
  } else if (event.type === "message" || event.type === "message-complete") {
151
+ // A saved turn can be outside the loaded page. Its transcript patch owns
152
+ // presentation; the following completion must not recreate it at the tail.
153
+ const persisted = event.type === "message" && event.status === "complete" && event.persisted === true;
154
+ if (persisted) changedPresentation = completed.delete(event.messageId) || changedPresentation;
151
155
  // A later initial read can already contain the result of buffered events.
152
156
  const older = streaming && event.streaming?.revision < streaming.revision;
153
157
  const completedWhileRunning = event.type === "message" && event.status === "complete" &&
@@ -155,13 +159,13 @@ function subscribeAssistantConversation({
155
159
  if (!older || completedWhileRunning) {
156
160
  const previous = streaming?.messages?.find(message => message.messageId === event.messageId);
157
161
  const changed = receiveStreaming(event.streaming);
158
- if (event.type === "message" && event.status === "complete" && event.text) {
162
+ if (event.type === "message" && event.status === "complete" && event.text && !persisted) {
159
163
  completed.set(event.messageId, { turnId: event.turnId, message: {
160
164
  messageId: event.messageId, role: event.role, text: event.text, status: "complete",
161
165
  ...(event.outputId ? { outputId: event.outputId } : {}),
162
166
  ...(event.turnId ? { turnId: event.turnId } : {}),
163
167
  ...(event.origin ? { origin: event.origin } : {}),
164
- ...(previous?.at ? { at: previous.at } : {})
168
+ ...((event.at || previous?.at) ? { at: event.at || previous.at } : {})
165
169
  } });
166
170
  }
167
171
  changedPresentation ||= changed || event.status === "complete";
@@ -2435,6 +2435,39 @@ test("canonical rewind replaces the loaded saved window without retaining remove
2435
2435
  });
2436
2436
 
2437
2437
 
2438
+ test("saved history replay stays off the live tail and loads once with its timestamp", options, async t => {
2439
+ const f = await fixture(t, 390);
2440
+ const progress = { role: "commentary", messageId: "replayed-progress", text: "Earlier saved progress",
2441
+ at: "2026-10-13T15:50:59.328Z" };
2442
+ const answer = { role: "assistant", messageId: "replayed-answer", text: "Earlier saved answer",
2443
+ at: "2026-10-13T15:52:58.929Z" };
2444
+ const turns = Array.from({ length: 37 }, (_, index) => {
2445
+ const user = { role: "user", messageId: `replay-question-${index}`, text: `Replay question ${index + 1}` };
2446
+ return { turnId: String(2485 + index).padStart(6, "0"), user,
2447
+ ...(index === 0 ? { commentary: [progress], assistant: answer, messages: [user, progress, answer] } : {}) };
2448
+ });
2449
+ await f.command("history", { id: "chat:1", turns, limit: 20 });
2450
+ await f.notify("chat:1");
2451
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().turns.value.length)).toBe(20);
2452
+ await f.page.evaluate(turn => {
2453
+ const socket = window.conversationFixture.socket;
2454
+ socket.notify("chat:1", { type: "transcript", patch: { type: "upsert-turn", turn } });
2455
+ for (const message of turn.messages.filter(message => message.role !== "user")) {
2456
+ socket.notify("chat:1", { type: "message", ...message, turnId: turn.turnId, status: "complete",
2457
+ persisted: true, streaming: { revision: 1, messages: [] } });
2458
+ }
2459
+ }, turns[0]);
2460
+ await expect(f.primary.getByText(answer.text, { exact: true })).toHaveCount(0);
2461
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().turns.value.length), 20);
2462
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
2463
+ await expect(f.primary.getByText(answer.text, { exact: true })).toHaveCount(1);
2464
+ const saved = await f.page.evaluate(() => window.conversationFixture.current().turns.value.find(turn => turn.turnId === "002485"));
2465
+ assert.equal(saved.user.text, "Replay question 1");
2466
+ assert.equal(saved.assistant.at, answer.at);
2467
+ assert.equal(saved.commentary[0].at, progress.at);
2468
+ assert.deepEqual(await f.sent(), [], "Reading replayed history cannot send another request");
2469
+ });
2470
+
2438
2471
  test("canonical loaded-history replay retains older completed output and its authored row", options, async t => {
2439
2472
  const f = await fixture(t, 390);
2440
2473
  const turns = Array.from({ length: 25 }, (_, index) => ({
@@ -570,6 +570,52 @@ test("stream snapshots preserve completed commentary and coalesce lifecycle read
570
570
  assert.deepEqual(errors, []);
571
571
  });
572
572
 
573
+ test("replayed saved completions outside the history page never become live tail rows", async t => {
574
+ const socket = new EventEmitter();
575
+ socket.connected = true;
576
+ const states = [], events = [];
577
+ const latest = Array.from({ length: 20 }, (_, index) => {
578
+ const turnId = String(2502 + index).padStart(6, "0");
579
+ const user = { messageId: `question-${turnId}`, role: "user", text: `Question ${turnId}` };
580
+ return { turnId, user, messages: [user] };
581
+ });
582
+ const initial = { id: "chat", status: "working", conversationLog: latest,
583
+ pagination: { limit: 20, hasMoreBefore: true }, streaming: { revision: 0, messages: [] } };
584
+ let subscriptionId, revision = 0;
585
+ socket.timeout = () => ({ emit(_name, input, acknowledge) {
586
+ subscriptionId = input.subscriptionId;
587
+ acknowledge(null, { ok: true, streamEpoch: "epoch", state: initial });
588
+ } });
589
+ const dispose = subscribeAssistantConversation({ socket, conversationId: "chat", targetSurfaceId: "home", hostSurfaceId: "home",
590
+ read: async () => initial, onState: state => states.push(state), onEvent: event => events.push(event) });
591
+ t.after(dispose);
592
+ function deliver(event) {
593
+ socket.emit(ASSISTANT_CONVERSATION_EVENT, { subscriptionId, conversationId: "chat", streamEpoch: "epoch",
594
+ streamRevision: ++revision, event });
595
+ }
596
+ const messages = [
597
+ { messageId: "old-progress", role: "commentary", text: "Earlier progress", at: "2026-10-13T15:50:59.328Z" },
598
+ { messageId: "old-answer", role: "assistant", text: "Earlier answer", at: "2026-10-13T15:52:58.929Z" }
599
+ ];
600
+ // Native history reconciliation first publishes the saved turn, then its
601
+ // completion notifications. Paging deliberately excludes this older turn.
602
+ const turn = { turnId: "002485", commentary: [messages[0]], assistant: messages[1], messages };
603
+ deliver({ type: "transcript", patch: { type: "upsert-turn", turn } });
604
+ for (const message of messages) deliver({ type: "message", ...message, turnId: turn.turnId,
605
+ status: "complete", persisted: true, streaming: initial.streaming });
606
+ assert.deepEqual(states.at(-1).turns, latest, "Saved replay cannot append a synthetic old turn after the current request");
607
+ await dispose.reload();
608
+ assert.deepEqual(states.at(-1).turns, latest, "Ordinary canonical refresh does not revive completed replay entries");
609
+ assert.equal(events.filter(event => event.type === "message").length, 2, "Notifications still reach non-transcript consumers");
610
+
611
+ const live = { messageId: "live-progress", turnId: "002521", role: "commentary", text: "New progress",
612
+ at: "2026-10-13T16:13:22.233Z", status: "complete" };
613
+ deliver({ type: "message", ...live, streaming: { revision: 1, messages: [] } });
614
+ assert.equal(states.at(-1).turns.at(-1).commentary[0].text, live.text);
615
+ assert.equal(states.at(-1).turns.at(-1).commentary[0].at, live.at,
616
+ "A completed message without an earlier partial retains its supplied timestamp");
617
+ });
618
+
573
619
  test("original transcript patches survive a read already in flight and the next canonical read remains authoritative", async t => {
574
620
  const changes = new EventEmitter();
575
621
  const socket = new EventEmitter();