@astralform/js 9.5.0 → 9.7.0

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/dist/index.cjs CHANGED
@@ -2825,6 +2825,48 @@ var ChatSession = class {
2825
2825
  if (!fresh.length) return;
2826
2826
  this.setMessages([...fresh, ...this.messages]);
2827
2827
  }
2828
+ /**
2829
+ * Re-fetch the NEWEST page of the conversation list and merge it in.
2830
+ *
2831
+ * The list the sidebar renders is seeded once at `connect()` and then only
2832
+ * changed by this client's own actions (create, delete, rename) — so a row's
2833
+ * `activeJobId` is as fresh as that seed, and a job started in ANOTHER tab or
2834
+ * on another device never appears, while one that ended elsewhere stays lit
2835
+ * forever. A caller that refreshes on a trigger (the tab becoming visible, or
2836
+ * a bounded interval while any job is live) keeps the rows honest without a
2837
+ * socket per conversation.
2838
+ *
2839
+ * Merge rules, and why each is what it is:
2840
+ *
2841
+ * - the server's ORDER wins for the rows it returns: an updated conversation
2842
+ * moves to its new place, which is the whole point of the call;
2843
+ * - rows paged in EARLIER (`loadMoreConversations`) are kept — this reads page
2844
+ * 1 only, and dropping the tail would make a refresh silently truncate a
2845
+ * list the reader has already scrolled;
2846
+ * - a row the caller MINTED locally and the server does not know yet is kept
2847
+ * too (it has no server row to be refreshed by);
2848
+ * - `conversationsGeneration` is bumped, for the same reason `connect()`
2849
+ * bumps it: a page request already in flight was cut against the old head
2850
+ * and its offset no longer means what it did.
2851
+ */
2852
+ async refreshConversations() {
2853
+ const page = await this.client.getConversations(CONVERSATION_PAGE_SIZE, 0);
2854
+ const pageIds = new Set(page.map((c) => c.id));
2855
+ const known = new Set(this.conversations.map((c) => c.id));
2856
+ this.conversationsGeneration++;
2857
+ this.conversations = [
2858
+ ...page,
2859
+ // Everything the page did not mention: older pages, and rows that exist
2860
+ // only on this client (a minted conversation the server has not been told
2861
+ // about yet).
2862
+ ...this.conversations.filter((c) => !pageIds.has(c.id))
2863
+ ];
2864
+ for (const c of page) {
2865
+ this.serverConversationIds.add(c.id);
2866
+ known.delete(c.id);
2867
+ }
2868
+ return page;
2869
+ }
2828
2870
  async loadMoreConversations() {
2829
2871
  if (this.isLoadingConversations || !this.hasMoreConversations) return [];
2830
2872
  this.isLoadingConversations = true;
@@ -3420,6 +3462,7 @@ var StreamManager = class _StreamManager {
3420
3462
  const haveByJobId = new Map(
3421
3463
  (have ?? []).map((h) => [h.jobId, h.lastSeq])
3422
3464
  );
3465
+ const heldFromMarks = (have ?? []).map((h) => h.messageId).filter((id) => typeof id === "string" && id.length > 0);
3423
3466
  const superseded = () => gen !== this.generation;
3424
3467
  if (superseded()) return;
3425
3468
  const turn = this.turnCounter;
@@ -3447,7 +3490,8 @@ var StreamManager = class _StreamManager {
3447
3490
  activeJobId,
3448
3491
  turn,
3449
3492
  jobsRequest,
3450
- haveByJobId
3493
+ haveByJobId,
3494
+ heldFromMarks
3451
3495
  ))
3452
3496
  return;
3453
3497
  if (activeJobId) {
@@ -3644,7 +3688,7 @@ var StreamManager = class _StreamManager {
3644
3688
  session.isLoadingEarlierTurns = false;
3645
3689
  }
3646
3690
  }
3647
- async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest, haveByJobId) {
3691
+ async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest, haveByJobId, heldFromMarks) {
3648
3692
  const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
3649
3693
  let complete = false;
3650
3694
  try {
@@ -3691,7 +3735,10 @@ var StreamManager = class _StreamManager {
3691
3735
  // primary cache scenario, a SETTLED conversation whose turns are all
3692
3736
  // held — and the empty-walk rescue would otherwise re-emit every one of
3693
3737
  // them as a steer bubble.
3694
- heldMessageIds: jobs.filter((j) => haveByJobId.has(j.job_id)).map((j) => j.message_id),
3738
+ heldMessageIds: [
3739
+ ...jobs.filter((j) => haveByJobId.has(j.job_id)).map((j) => j.message_id),
3740
+ ...heldFromMarks
3741
+ ],
3695
3742
  userMessages: userMessages.map((m) => ({
3696
3743
  id: m.id,
3697
3744
  content: m.content
@@ -3725,8 +3772,11 @@ var StreamManager = class _StreamManager {
3725
3772
  step.messageId
3726
3773
  );
3727
3774
  }
3728
- this.session.hasMoreTurns = page.hasMore;
3729
- this.session.oldestTurnCursor = page.nextBefore;
3775
+ const drewNothing = plan.length === 0;
3776
+ const holdsTurns = haveByJobId.size > 0;
3777
+ const noDrawableTurns = drewNothing && holdsTurns;
3778
+ this.session.hasMoreTurns = noDrawableTurns ? false : page.hasMore;
3779
+ this.session.oldestTurnCursor = noDrawableTurns ? null : page.nextBefore;
3730
3780
  this.oldestDrawnMessageId = plan[0]?.messageId ?? null;
3731
3781
  complete = true;
3732
3782
  if (stopReplay()) return false;