@astralform/js 4.2.0 → 4.3.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
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  AstralformClient: () => AstralformClient,
24
24
  AstralformError: () => AstralformError,
25
25
  AuthenticationError: () => AuthenticationError,
26
+ CONVERSATION_PAGE_SIZE: () => CONVERSATION_PAGE_SIZE,
26
27
  ChatEventType: () => ChatEventType,
27
28
  ChatSession: () => ChatSession,
28
29
  ConnectionError: () => ConnectionError,
@@ -353,7 +354,7 @@ var AstralformClient = class {
353
354
  }
354
355
  /**
355
356
  * Replace the current OIDC access token without reconstructing the client.
356
- * Use after refreshing via the host's token manager (e.g., Supabase JS SDK).
357
+ * Use after refreshing via the host app's own token manager.
357
358
  * Throws if the client was created in API-key mode.
358
359
  */
359
360
  updateAccessToken(accessToken) {
@@ -1278,6 +1279,7 @@ function translateWireEvent(wire) {
1278
1279
  // src/session.ts
1279
1280
  var SSE_MAX_RECONNECTS = 6;
1280
1281
  var TOOL_RESULT_MAX_RETRIES = 3;
1282
+ var CONVERSATION_PAGE_SIZE = 50;
1281
1283
  function sseReconnectDelayMs(attempt) {
1282
1284
  return Math.min(500 * 2 ** (attempt - 1), 5e3);
1283
1285
  }
@@ -1301,6 +1303,17 @@ var ChatSession = class {
1301
1303
  // State
1302
1304
  this.conversationId = null;
1303
1305
  this.conversations = [];
1306
+ /**
1307
+ * Whether another page of conversations may exist on the server.
1308
+ *
1309
+ * Inferred from the last page being full, since the list endpoint returns a
1310
+ * bare array with no total. A total that happens to be an exact multiple of
1311
+ * the page size therefore costs one extra empty request before this flips —
1312
+ * cheaper than adding a count query to every list call.
1313
+ */
1314
+ this.hasMoreConversations = false;
1315
+ /** True while ``loadMoreConversations`` is in flight. */
1316
+ this.isLoadingConversations = false;
1304
1317
  this.messages = [];
1305
1318
  this.isStreaming = false;
1306
1319
  this.agentStatus = null;
@@ -1308,6 +1321,32 @@ var ChatSession = class {
1308
1321
  this.skills = [];
1309
1322
  this.enabledClientTools = /* @__PURE__ */ new Set();
1310
1323
  this.modelDisplayName = null;
1324
+ /**
1325
+ * Ids of conversations the SERVER has handed us, which is the paging offset.
1326
+ *
1327
+ * Deliberately not ``conversations.length``. That array also holds
1328
+ * conversations created locally and unshifted on top (``createNewConversation``,
1329
+ * and the auto-created conversation in ``consumeJobStream``), so using its
1330
+ * length as the offset would over-count and silently skip a row of real
1331
+ * history on the next page. Tracking ids rather than a counter also makes
1332
+ * deletion self-correcting: removing a server-sourced conversation shifts
1333
+ * every later page up by one, and dropping its id from this set is exactly
1334
+ * that shift — while deleting a purely local one correctly changes nothing.
1335
+ */
1336
+ this.serverConversationIds = /* @__PURE__ */ new Set();
1337
+ /**
1338
+ * Bumped every time ``connect()`` re-seeds the conversation list.
1339
+ *
1340
+ * A ``loadMoreConversations`` request issued before a re-seed describes the
1341
+ * OLD paging state, so applying its response afterwards both appends the
1342
+ * wrong rows and corrupts the offset. Concretely: with 100 rows held, an
1343
+ * offset-100 response landing after a reconnect has reset to rows 0-49 would
1344
+ * append rows 100-149 — a 50-row hole — and leave the id set at 100, so every
1345
+ * later page re-requests offset 100 and never advances again. The generation
1346
+ * is captured before the await and rechecked after, so a superseded response
1347
+ * is discarded instead.
1348
+ */
1349
+ this.conversationsGeneration = 0;
1311
1350
  // Minimal in-session accumulation for the assistant message record.
1312
1351
  // Only top-level ``text`` blocks contribute; subagent / tool output
1313
1352
  // is tracked by the consumer's own block store.
@@ -1347,7 +1386,7 @@ var ChatSession = class {
1347
1386
  async connect() {
1348
1387
  const [status, conversations, agents, skills] = await Promise.allSettled([
1349
1388
  this.client.getAgentStatus(),
1350
- this.client.getConversations(),
1389
+ this.client.getConversations(CONVERSATION_PAGE_SIZE),
1351
1390
  this.client.getAgents().catch(() => []),
1352
1391
  this.client.getSkills().catch(() => [])
1353
1392
  ]);
@@ -1355,7 +1394,12 @@ var ChatSession = class {
1355
1394
  this.agentStatus = status.value;
1356
1395
  }
1357
1396
  if (conversations.status === "fulfilled") {
1397
+ this.conversationsGeneration++;
1358
1398
  this.conversations = conversations.value;
1399
+ this.serverConversationIds = new Set(
1400
+ conversations.value.map((c) => c.id)
1401
+ );
1402
+ this.hasMoreConversations = conversations.value.length === CONVERSATION_PAGE_SIZE;
1359
1403
  }
1360
1404
  if (agents.status === "fulfilled") {
1361
1405
  this.agents = agents.value;
@@ -1814,12 +1858,69 @@ var ChatSession = class {
1814
1858
  eventsResult.status === "fulfilled" ? eventsResult.value : []
1815
1859
  );
1816
1860
  }
1861
+ /**
1862
+ * Append the next page of conversation history to ``conversations``.
1863
+ *
1864
+ * The list is ordered ``updated_at DESC`` and paged by offset, so a
1865
+ * conversation bumped to the top mid-scroll can surface again in a later
1866
+ * page; ids already held are dropped rather than duplicated. Returns only
1867
+ * the conversations actually appended, which may be empty even on a full
1868
+ * page. Rejects on network failure with ``hasMoreConversations`` still true,
1869
+ * so the caller can retry.
1870
+ *
1871
+ * KNOWN LIMITATION — offset paging is only stable while the prefix already
1872
+ * consumed stays put. The offset tracking here corrects for perturbations
1873
+ * THIS session causes (local unshifts, ``deleteConversation``), but not for
1874
+ * ones it never sees:
1875
+ *
1876
+ * - a conversation this session hasn't loaded yet is bumped to the top (a
1877
+ * headless routine or another device posting to it), pushing the whole
1878
+ * list down — it lands inside the consumed prefix, which no later offset
1879
+ * revisits;
1880
+ * - a conversation is deleted from another tab/device, shrinking the list so
1881
+ * the next offset lands one row too far in.
1882
+ *
1883
+ * Each perturbation costs at most one conversation off the sidebar, and only
1884
+ * until the next ``connect()`` — that re-seeds page 1 and resets the paging
1885
+ * state, so a reload or reconnect always recovers it. Nothing is lost
1886
+ * server-side. Both cases are pinned by tests in
1887
+ * ``tests/conversation-paging.test.ts``.
1888
+ *
1889
+ * Closing the gap properly needs a stable server cursor (keyset paging on
1890
+ * ``(updated_at, id)``) rather than a raw offset, which is a backend change —
1891
+ * tracking ids client-side cannot discover a row that moved into a region
1892
+ * already scanned.
1893
+ */
1894
+ async loadMoreConversations() {
1895
+ if (this.isLoadingConversations || !this.hasMoreConversations) return [];
1896
+ this.isLoadingConversations = true;
1897
+ const generation = this.conversationsGeneration;
1898
+ try {
1899
+ const page = await this.client.getConversations(
1900
+ CONVERSATION_PAGE_SIZE,
1901
+ this.serverConversationIds.size
1902
+ );
1903
+ if (generation !== this.conversationsGeneration) return [];
1904
+ this.hasMoreConversations = page.length === CONVERSATION_PAGE_SIZE;
1905
+ const fresh = page.filter((c) => !this.serverConversationIds.has(c.id));
1906
+ for (const c of page) this.serverConversationIds.add(c.id);
1907
+ const known = new Set(this.conversations.map((c) => c.id));
1908
+ const appended = fresh.filter((c) => !known.has(c.id));
1909
+ this.conversations.push(...appended);
1910
+ return appended;
1911
+ } finally {
1912
+ this.isLoadingConversations = false;
1913
+ }
1914
+ }
1817
1915
  async deleteConversation(id) {
1818
1916
  try {
1819
1917
  await this.client.deleteConversation(id);
1820
1918
  } catch {
1821
1919
  }
1822
1920
  await this.storage.deleteConversation(id);
1921
+ if (this.serverConversationIds.delete(id)) {
1922
+ this.conversationsGeneration++;
1923
+ }
1823
1924
  this.conversations = this.conversations.filter((c) => c.id !== id);
1824
1925
  if (this.conversationId === id) {
1825
1926
  this.conversationId = null;
@@ -2205,6 +2306,7 @@ function parseEmbeddedResource(value) {
2205
2306
  AstralformClient,
2206
2307
  AstralformError,
2207
2308
  AuthenticationError,
2309
+ CONVERSATION_PAGE_SIZE,
2208
2310
  ChatEventType,
2209
2311
  ChatSession,
2210
2312
  ConnectionError,