@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.d.cts CHANGED
@@ -796,7 +796,7 @@ declare class AstralformClient {
796
796
  constructor(config: AstralformConfig);
797
797
  /**
798
798
  * Replace the current OIDC access token without reconstructing the client.
799
- * Use after refreshing via the host's token manager (e.g., Supabase JS SDK).
799
+ * Use after refreshing via the host app's own token manager.
800
800
  * Throws if the client was created in API-key mode.
801
801
  */
802
802
  updateAccessToken(accessToken: string): void;
@@ -980,6 +980,13 @@ declare class ToolRegistry {
980
980
  }
981
981
 
982
982
  type ChatEventHandler = (event: ChatEvent) => void;
983
+ /**
984
+ * Conversations fetched per page, by ``connect`` and ``loadMoreConversations``
985
+ * alike. The two must use the same size: the offset is derived from how many
986
+ * rows the server has returned so far, so a first page of a different size
987
+ * would leave the second page's offset pointing at the wrong row.
988
+ */
989
+ declare const CONVERSATION_PAGE_SIZE = 50;
983
990
  /**
984
991
  * ChatSession — translates the backend wire protocol into typed ChatEvents
985
992
  * for consumers. Owns HTTP + SSE plumbing, conversation state, and the
@@ -1000,6 +1007,17 @@ declare class ChatSession {
1000
1007
  readonly protocols: ProtocolRegistry<ProtocolAdapter>;
1001
1008
  conversationId: string | null;
1002
1009
  conversations: Conversation[];
1010
+ /**
1011
+ * Whether another page of conversations may exist on the server.
1012
+ *
1013
+ * Inferred from the last page being full, since the list endpoint returns a
1014
+ * bare array with no total. A total that happens to be an exact multiple of
1015
+ * the page size therefore costs one extra empty request before this flips —
1016
+ * cheaper than adding a count query to every list call.
1017
+ */
1018
+ hasMoreConversations: boolean;
1019
+ /** True while ``loadMoreConversations`` is in flight. */
1020
+ isLoadingConversations: boolean;
1003
1021
  messages: Message[];
1004
1022
  isStreaming: boolean;
1005
1023
  agentStatus: AgentStatus | null;
@@ -1007,6 +1025,32 @@ declare class ChatSession {
1007
1025
  skills: SkillInfo[];
1008
1026
  enabledClientTools: Set<string>;
1009
1027
  modelDisplayName: string | null;
1028
+ /**
1029
+ * Ids of conversations the SERVER has handed us, which is the paging offset.
1030
+ *
1031
+ * Deliberately not ``conversations.length``. That array also holds
1032
+ * conversations created locally and unshifted on top (``createNewConversation``,
1033
+ * and the auto-created conversation in ``consumeJobStream``), so using its
1034
+ * length as the offset would over-count and silently skip a row of real
1035
+ * history on the next page. Tracking ids rather than a counter also makes
1036
+ * deletion self-correcting: removing a server-sourced conversation shifts
1037
+ * every later page up by one, and dropping its id from this set is exactly
1038
+ * that shift — while deleting a purely local one correctly changes nothing.
1039
+ */
1040
+ private serverConversationIds;
1041
+ /**
1042
+ * Bumped every time ``connect()`` re-seeds the conversation list.
1043
+ *
1044
+ * A ``loadMoreConversations`` request issued before a re-seed describes the
1045
+ * OLD paging state, so applying its response afterwards both appends the
1046
+ * wrong rows and corrupts the offset. Concretely: with 100 rows held, an
1047
+ * offset-100 response landing after a reconnect has reset to rows 0-49 would
1048
+ * append rows 100-149 — a 50-row hole — and leave the id set at 100, so every
1049
+ * later page re-requests offset 100 and never advances again. The generation
1050
+ * is captured before the await and rechecked after, so a superseded response
1051
+ * is discarded instead.
1052
+ */
1053
+ private conversationsGeneration;
1010
1054
  private accumulatedText;
1011
1055
  private currentTextPath;
1012
1056
  private handlers;
@@ -1108,6 +1152,40 @@ declare class ChatSession {
1108
1152
  * job's events.
1109
1153
  */
1110
1154
  switchConversation(id: string, jobId?: string): Promise<void>;
1155
+ /**
1156
+ * Append the next page of conversation history to ``conversations``.
1157
+ *
1158
+ * The list is ordered ``updated_at DESC`` and paged by offset, so a
1159
+ * conversation bumped to the top mid-scroll can surface again in a later
1160
+ * page; ids already held are dropped rather than duplicated. Returns only
1161
+ * the conversations actually appended, which may be empty even on a full
1162
+ * page. Rejects on network failure with ``hasMoreConversations`` still true,
1163
+ * so the caller can retry.
1164
+ *
1165
+ * KNOWN LIMITATION — offset paging is only stable while the prefix already
1166
+ * consumed stays put. The offset tracking here corrects for perturbations
1167
+ * THIS session causes (local unshifts, ``deleteConversation``), but not for
1168
+ * ones it never sees:
1169
+ *
1170
+ * - a conversation this session hasn't loaded yet is bumped to the top (a
1171
+ * headless routine or another device posting to it), pushing the whole
1172
+ * list down — it lands inside the consumed prefix, which no later offset
1173
+ * revisits;
1174
+ * - a conversation is deleted from another tab/device, shrinking the list so
1175
+ * the next offset lands one row too far in.
1176
+ *
1177
+ * Each perturbation costs at most one conversation off the sidebar, and only
1178
+ * until the next ``connect()`` — that re-seeds page 1 and resets the paging
1179
+ * state, so a reload or reconnect always recovers it. Nothing is lost
1180
+ * server-side. Both cases are pinned by tests in
1181
+ * ``tests/conversation-paging.test.ts``.
1182
+ *
1183
+ * Closing the gap properly needs a stable server cursor (keyset paging on
1184
+ * ``(updated_at, id)``) rather than a raw offset, which is a backend change —
1185
+ * tracking ids client-side cannot discover a row that moved into a region
1186
+ * already scanned.
1187
+ */
1188
+ loadMoreConversations(): Promise<Conversation[]>;
1111
1189
  deleteConversation(id: string): Promise<void>;
1112
1190
  toggleClientTool(name: string): boolean;
1113
1191
  }
@@ -1340,4 +1418,4 @@ declare function isEmbeddedResource(value: unknown): value is {
1340
1418
  */
1341
1419
  declare function parseEmbeddedResource(value: unknown): EmbeddedResource | null;
1342
1420
 
1343
- export { type ActiveJob, type AgentIdentity, type AgentInfo, type AgentStatus, type AssetCreatedPayload, type AstralformApiKeyConfig, AstralformClient, type AstralformConfig, AstralformError, type AstralformUserTokenConfig, type AttachmentStagedPayload, AuthenticationError, type BlockDeltaPayload, type ChatEvent, ChatEventType, type ChatEventTypeValue, ChatSession, type ChatStorage, type ChatStreamEvent, type ChatStreamRequest, ConnectionError, type ContextUpdatePayload, type ContextWarningPayload, type Conversation, type ConversationAsset, type ConversationEvent, type DesktopStreamPayload, type EmbeddedResource, type FeedbackRequest, type FeedbackResponse, InMemoryStorage, type JobCreateResponse, type JobStatus, type JobSummary, LLMNotConfiguredError, type MemoryRecallPayload, type MemoryRecord, type MemoryUpdatePayload, type Message, type ModelChoiceOptions, type ModelOption, type MyToolGrantsPage, type PromptSuggestionPayload, type ProtocolAdapter, ProtocolRegistry, RateLimitError, type RateLimitErrorDetails, type RawSseEvent, type ReasoningEffort, type SendOptions, ServerError, type SkillInfo, StreamAbortedError, type StreamJobSSEOptions, StreamManager, type StreamManagerEvent, type StreamState, type SubagentStartPayload, type SubagentStopPayload, type TaskStatus, type TeamAgentSummary, type TeamSummary, type TitleGeneratedPayload, type TodoItem, type TodoUpdatePayload, type ToolApprovalDecision, type ToolApprovalGrantedPayload, type ToolApprovalRequest, type ToolApprovalRequestedPayload, type ToolApprovalScope, type ToolCallRequest, type ToolDefinition, type ToolGrant, type ToolHandler, type ToolHarnessWarningPayload, type ToolPermissionDeniedPayload, ToolRegistry, type ToolResult, type ToolResultRequest, type TurnUsage, type UIComponentsConfig, type UserUnavailablePayload, type WireBlockDelta, type WireBlockDeltaPayload, type WireBlockKind, type WireBlockStart, type WireBlockStatus, type WireBlockStop, type WireCustomEvent, type WireErrorEvent, type WireEvent, type WireInputArgDelta, type WireInputDelta, type WireKeepalive, type WireMessageStart, type WireMessageStop, type WireOutputDelta, type WireRetryEvent, type WireSignatureDelta, type WireStallWarning, type WireStatusDelta, type WireStopReason, type WireTextDelta, type WireThinkingDelta, type WorkspaceReadyPayload, generateId, isEmbeddedResource, mapSseToChat, parseEmbeddedResource, replayEvents, streamJobSSE, translateDelta };
1421
+ export { type ActiveJob, type AgentIdentity, type AgentInfo, type AgentStatus, type AssetCreatedPayload, type AstralformApiKeyConfig, AstralformClient, type AstralformConfig, AstralformError, type AstralformUserTokenConfig, type AttachmentStagedPayload, AuthenticationError, type BlockDeltaPayload, CONVERSATION_PAGE_SIZE, type ChatEvent, ChatEventType, type ChatEventTypeValue, ChatSession, type ChatStorage, type ChatStreamEvent, type ChatStreamRequest, ConnectionError, type ContextUpdatePayload, type ContextWarningPayload, type Conversation, type ConversationAsset, type ConversationEvent, type DesktopStreamPayload, type EmbeddedResource, type FeedbackRequest, type FeedbackResponse, InMemoryStorage, type JobCreateResponse, type JobStatus, type JobSummary, LLMNotConfiguredError, type MemoryRecallPayload, type MemoryRecord, type MemoryUpdatePayload, type Message, type ModelChoiceOptions, type ModelOption, type MyToolGrantsPage, type PromptSuggestionPayload, type ProtocolAdapter, ProtocolRegistry, RateLimitError, type RateLimitErrorDetails, type RawSseEvent, type ReasoningEffort, type SendOptions, ServerError, type SkillInfo, StreamAbortedError, type StreamJobSSEOptions, StreamManager, type StreamManagerEvent, type StreamState, type SubagentStartPayload, type SubagentStopPayload, type TaskStatus, type TeamAgentSummary, type TeamSummary, type TitleGeneratedPayload, type TodoItem, type TodoUpdatePayload, type ToolApprovalDecision, type ToolApprovalGrantedPayload, type ToolApprovalRequest, type ToolApprovalRequestedPayload, type ToolApprovalScope, type ToolCallRequest, type ToolDefinition, type ToolGrant, type ToolHandler, type ToolHarnessWarningPayload, type ToolPermissionDeniedPayload, ToolRegistry, type ToolResult, type ToolResultRequest, type TurnUsage, type UIComponentsConfig, type UserUnavailablePayload, type WireBlockDelta, type WireBlockDeltaPayload, type WireBlockKind, type WireBlockStart, type WireBlockStatus, type WireBlockStop, type WireCustomEvent, type WireErrorEvent, type WireEvent, type WireInputArgDelta, type WireInputDelta, type WireKeepalive, type WireMessageStart, type WireMessageStop, type WireOutputDelta, type WireRetryEvent, type WireSignatureDelta, type WireStallWarning, type WireStatusDelta, type WireStopReason, type WireTextDelta, type WireThinkingDelta, type WorkspaceReadyPayload, generateId, isEmbeddedResource, mapSseToChat, parseEmbeddedResource, replayEvents, streamJobSSE, translateDelta };
package/dist/index.d.ts CHANGED
@@ -796,7 +796,7 @@ declare class AstralformClient {
796
796
  constructor(config: AstralformConfig);
797
797
  /**
798
798
  * Replace the current OIDC access token without reconstructing the client.
799
- * Use after refreshing via the host's token manager (e.g., Supabase JS SDK).
799
+ * Use after refreshing via the host app's own token manager.
800
800
  * Throws if the client was created in API-key mode.
801
801
  */
802
802
  updateAccessToken(accessToken: string): void;
@@ -980,6 +980,13 @@ declare class ToolRegistry {
980
980
  }
981
981
 
982
982
  type ChatEventHandler = (event: ChatEvent) => void;
983
+ /**
984
+ * Conversations fetched per page, by ``connect`` and ``loadMoreConversations``
985
+ * alike. The two must use the same size: the offset is derived from how many
986
+ * rows the server has returned so far, so a first page of a different size
987
+ * would leave the second page's offset pointing at the wrong row.
988
+ */
989
+ declare const CONVERSATION_PAGE_SIZE = 50;
983
990
  /**
984
991
  * ChatSession — translates the backend wire protocol into typed ChatEvents
985
992
  * for consumers. Owns HTTP + SSE plumbing, conversation state, and the
@@ -1000,6 +1007,17 @@ declare class ChatSession {
1000
1007
  readonly protocols: ProtocolRegistry<ProtocolAdapter>;
1001
1008
  conversationId: string | null;
1002
1009
  conversations: Conversation[];
1010
+ /**
1011
+ * Whether another page of conversations may exist on the server.
1012
+ *
1013
+ * Inferred from the last page being full, since the list endpoint returns a
1014
+ * bare array with no total. A total that happens to be an exact multiple of
1015
+ * the page size therefore costs one extra empty request before this flips —
1016
+ * cheaper than adding a count query to every list call.
1017
+ */
1018
+ hasMoreConversations: boolean;
1019
+ /** True while ``loadMoreConversations`` is in flight. */
1020
+ isLoadingConversations: boolean;
1003
1021
  messages: Message[];
1004
1022
  isStreaming: boolean;
1005
1023
  agentStatus: AgentStatus | null;
@@ -1007,6 +1025,32 @@ declare class ChatSession {
1007
1025
  skills: SkillInfo[];
1008
1026
  enabledClientTools: Set<string>;
1009
1027
  modelDisplayName: string | null;
1028
+ /**
1029
+ * Ids of conversations the SERVER has handed us, which is the paging offset.
1030
+ *
1031
+ * Deliberately not ``conversations.length``. That array also holds
1032
+ * conversations created locally and unshifted on top (``createNewConversation``,
1033
+ * and the auto-created conversation in ``consumeJobStream``), so using its
1034
+ * length as the offset would over-count and silently skip a row of real
1035
+ * history on the next page. Tracking ids rather than a counter also makes
1036
+ * deletion self-correcting: removing a server-sourced conversation shifts
1037
+ * every later page up by one, and dropping its id from this set is exactly
1038
+ * that shift — while deleting a purely local one correctly changes nothing.
1039
+ */
1040
+ private serverConversationIds;
1041
+ /**
1042
+ * Bumped every time ``connect()`` re-seeds the conversation list.
1043
+ *
1044
+ * A ``loadMoreConversations`` request issued before a re-seed describes the
1045
+ * OLD paging state, so applying its response afterwards both appends the
1046
+ * wrong rows and corrupts the offset. Concretely: with 100 rows held, an
1047
+ * offset-100 response landing after a reconnect has reset to rows 0-49 would
1048
+ * append rows 100-149 — a 50-row hole — and leave the id set at 100, so every
1049
+ * later page re-requests offset 100 and never advances again. The generation
1050
+ * is captured before the await and rechecked after, so a superseded response
1051
+ * is discarded instead.
1052
+ */
1053
+ private conversationsGeneration;
1010
1054
  private accumulatedText;
1011
1055
  private currentTextPath;
1012
1056
  private handlers;
@@ -1108,6 +1152,40 @@ declare class ChatSession {
1108
1152
  * job's events.
1109
1153
  */
1110
1154
  switchConversation(id: string, jobId?: string): Promise<void>;
1155
+ /**
1156
+ * Append the next page of conversation history to ``conversations``.
1157
+ *
1158
+ * The list is ordered ``updated_at DESC`` and paged by offset, so a
1159
+ * conversation bumped to the top mid-scroll can surface again in a later
1160
+ * page; ids already held are dropped rather than duplicated. Returns only
1161
+ * the conversations actually appended, which may be empty even on a full
1162
+ * page. Rejects on network failure with ``hasMoreConversations`` still true,
1163
+ * so the caller can retry.
1164
+ *
1165
+ * KNOWN LIMITATION — offset paging is only stable while the prefix already
1166
+ * consumed stays put. The offset tracking here corrects for perturbations
1167
+ * THIS session causes (local unshifts, ``deleteConversation``), but not for
1168
+ * ones it never sees:
1169
+ *
1170
+ * - a conversation this session hasn't loaded yet is bumped to the top (a
1171
+ * headless routine or another device posting to it), pushing the whole
1172
+ * list down — it lands inside the consumed prefix, which no later offset
1173
+ * revisits;
1174
+ * - a conversation is deleted from another tab/device, shrinking the list so
1175
+ * the next offset lands one row too far in.
1176
+ *
1177
+ * Each perturbation costs at most one conversation off the sidebar, and only
1178
+ * until the next ``connect()`` — that re-seeds page 1 and resets the paging
1179
+ * state, so a reload or reconnect always recovers it. Nothing is lost
1180
+ * server-side. Both cases are pinned by tests in
1181
+ * ``tests/conversation-paging.test.ts``.
1182
+ *
1183
+ * Closing the gap properly needs a stable server cursor (keyset paging on
1184
+ * ``(updated_at, id)``) rather than a raw offset, which is a backend change —
1185
+ * tracking ids client-side cannot discover a row that moved into a region
1186
+ * already scanned.
1187
+ */
1188
+ loadMoreConversations(): Promise<Conversation[]>;
1111
1189
  deleteConversation(id: string): Promise<void>;
1112
1190
  toggleClientTool(name: string): boolean;
1113
1191
  }
@@ -1340,4 +1418,4 @@ declare function isEmbeddedResource(value: unknown): value is {
1340
1418
  */
1341
1419
  declare function parseEmbeddedResource(value: unknown): EmbeddedResource | null;
1342
1420
 
1343
- export { type ActiveJob, type AgentIdentity, type AgentInfo, type AgentStatus, type AssetCreatedPayload, type AstralformApiKeyConfig, AstralformClient, type AstralformConfig, AstralformError, type AstralformUserTokenConfig, type AttachmentStagedPayload, AuthenticationError, type BlockDeltaPayload, type ChatEvent, ChatEventType, type ChatEventTypeValue, ChatSession, type ChatStorage, type ChatStreamEvent, type ChatStreamRequest, ConnectionError, type ContextUpdatePayload, type ContextWarningPayload, type Conversation, type ConversationAsset, type ConversationEvent, type DesktopStreamPayload, type EmbeddedResource, type FeedbackRequest, type FeedbackResponse, InMemoryStorage, type JobCreateResponse, type JobStatus, type JobSummary, LLMNotConfiguredError, type MemoryRecallPayload, type MemoryRecord, type MemoryUpdatePayload, type Message, type ModelChoiceOptions, type ModelOption, type MyToolGrantsPage, type PromptSuggestionPayload, type ProtocolAdapter, ProtocolRegistry, RateLimitError, type RateLimitErrorDetails, type RawSseEvent, type ReasoningEffort, type SendOptions, ServerError, type SkillInfo, StreamAbortedError, type StreamJobSSEOptions, StreamManager, type StreamManagerEvent, type StreamState, type SubagentStartPayload, type SubagentStopPayload, type TaskStatus, type TeamAgentSummary, type TeamSummary, type TitleGeneratedPayload, type TodoItem, type TodoUpdatePayload, type ToolApprovalDecision, type ToolApprovalGrantedPayload, type ToolApprovalRequest, type ToolApprovalRequestedPayload, type ToolApprovalScope, type ToolCallRequest, type ToolDefinition, type ToolGrant, type ToolHandler, type ToolHarnessWarningPayload, type ToolPermissionDeniedPayload, ToolRegistry, type ToolResult, type ToolResultRequest, type TurnUsage, type UIComponentsConfig, type UserUnavailablePayload, type WireBlockDelta, type WireBlockDeltaPayload, type WireBlockKind, type WireBlockStart, type WireBlockStatus, type WireBlockStop, type WireCustomEvent, type WireErrorEvent, type WireEvent, type WireInputArgDelta, type WireInputDelta, type WireKeepalive, type WireMessageStart, type WireMessageStop, type WireOutputDelta, type WireRetryEvent, type WireSignatureDelta, type WireStallWarning, type WireStatusDelta, type WireStopReason, type WireTextDelta, type WireThinkingDelta, type WorkspaceReadyPayload, generateId, isEmbeddedResource, mapSseToChat, parseEmbeddedResource, replayEvents, streamJobSSE, translateDelta };
1421
+ export { type ActiveJob, type AgentIdentity, type AgentInfo, type AgentStatus, type AssetCreatedPayload, type AstralformApiKeyConfig, AstralformClient, type AstralformConfig, AstralformError, type AstralformUserTokenConfig, type AttachmentStagedPayload, AuthenticationError, type BlockDeltaPayload, CONVERSATION_PAGE_SIZE, type ChatEvent, ChatEventType, type ChatEventTypeValue, ChatSession, type ChatStorage, type ChatStreamEvent, type ChatStreamRequest, ConnectionError, type ContextUpdatePayload, type ContextWarningPayload, type Conversation, type ConversationAsset, type ConversationEvent, type DesktopStreamPayload, type EmbeddedResource, type FeedbackRequest, type FeedbackResponse, InMemoryStorage, type JobCreateResponse, type JobStatus, type JobSummary, LLMNotConfiguredError, type MemoryRecallPayload, type MemoryRecord, type MemoryUpdatePayload, type Message, type ModelChoiceOptions, type ModelOption, type MyToolGrantsPage, type PromptSuggestionPayload, type ProtocolAdapter, ProtocolRegistry, RateLimitError, type RateLimitErrorDetails, type RawSseEvent, type ReasoningEffort, type SendOptions, ServerError, type SkillInfo, StreamAbortedError, type StreamJobSSEOptions, StreamManager, type StreamManagerEvent, type StreamState, type SubagentStartPayload, type SubagentStopPayload, type TaskStatus, type TeamAgentSummary, type TeamSummary, type TitleGeneratedPayload, type TodoItem, type TodoUpdatePayload, type ToolApprovalDecision, type ToolApprovalGrantedPayload, type ToolApprovalRequest, type ToolApprovalRequestedPayload, type ToolApprovalScope, type ToolCallRequest, type ToolDefinition, type ToolGrant, type ToolHandler, type ToolHarnessWarningPayload, type ToolPermissionDeniedPayload, ToolRegistry, type ToolResult, type ToolResultRequest, type TurnUsage, type UIComponentsConfig, type UserUnavailablePayload, type WireBlockDelta, type WireBlockDeltaPayload, type WireBlockKind, type WireBlockStart, type WireBlockStatus, type WireBlockStop, type WireCustomEvent, type WireErrorEvent, type WireEvent, type WireInputArgDelta, type WireInputDelta, type WireKeepalive, type WireMessageStart, type WireMessageStop, type WireOutputDelta, type WireRetryEvent, type WireSignatureDelta, type WireStallWarning, type WireStatusDelta, type WireStopReason, type WireTextDelta, type WireThinkingDelta, type WorkspaceReadyPayload, generateId, isEmbeddedResource, mapSseToChat, parseEmbeddedResource, replayEvents, streamJobSSE, translateDelta };
package/dist/index.js CHANGED
@@ -307,7 +307,7 @@ var AstralformClient = class {
307
307
  }
308
308
  /**
309
309
  * Replace the current OIDC access token without reconstructing the client.
310
- * Use after refreshing via the host's token manager (e.g., Supabase JS SDK).
310
+ * Use after refreshing via the host app's own token manager.
311
311
  * Throws if the client was created in API-key mode.
312
312
  */
313
313
  updateAccessToken(accessToken) {
@@ -1232,6 +1232,7 @@ function translateWireEvent(wire) {
1232
1232
  // src/session.ts
1233
1233
  var SSE_MAX_RECONNECTS = 6;
1234
1234
  var TOOL_RESULT_MAX_RETRIES = 3;
1235
+ var CONVERSATION_PAGE_SIZE = 50;
1235
1236
  function sseReconnectDelayMs(attempt) {
1236
1237
  return Math.min(500 * 2 ** (attempt - 1), 5e3);
1237
1238
  }
@@ -1255,6 +1256,17 @@ var ChatSession = class {
1255
1256
  // State
1256
1257
  this.conversationId = null;
1257
1258
  this.conversations = [];
1259
+ /**
1260
+ * Whether another page of conversations may exist on the server.
1261
+ *
1262
+ * Inferred from the last page being full, since the list endpoint returns a
1263
+ * bare array with no total. A total that happens to be an exact multiple of
1264
+ * the page size therefore costs one extra empty request before this flips —
1265
+ * cheaper than adding a count query to every list call.
1266
+ */
1267
+ this.hasMoreConversations = false;
1268
+ /** True while ``loadMoreConversations`` is in flight. */
1269
+ this.isLoadingConversations = false;
1258
1270
  this.messages = [];
1259
1271
  this.isStreaming = false;
1260
1272
  this.agentStatus = null;
@@ -1262,6 +1274,32 @@ var ChatSession = class {
1262
1274
  this.skills = [];
1263
1275
  this.enabledClientTools = /* @__PURE__ */ new Set();
1264
1276
  this.modelDisplayName = null;
1277
+ /**
1278
+ * Ids of conversations the SERVER has handed us, which is the paging offset.
1279
+ *
1280
+ * Deliberately not ``conversations.length``. That array also holds
1281
+ * conversations created locally and unshifted on top (``createNewConversation``,
1282
+ * and the auto-created conversation in ``consumeJobStream``), so using its
1283
+ * length as the offset would over-count and silently skip a row of real
1284
+ * history on the next page. Tracking ids rather than a counter also makes
1285
+ * deletion self-correcting: removing a server-sourced conversation shifts
1286
+ * every later page up by one, and dropping its id from this set is exactly
1287
+ * that shift — while deleting a purely local one correctly changes nothing.
1288
+ */
1289
+ this.serverConversationIds = /* @__PURE__ */ new Set();
1290
+ /**
1291
+ * Bumped every time ``connect()`` re-seeds the conversation list.
1292
+ *
1293
+ * A ``loadMoreConversations`` request issued before a re-seed describes the
1294
+ * OLD paging state, so applying its response afterwards both appends the
1295
+ * wrong rows and corrupts the offset. Concretely: with 100 rows held, an
1296
+ * offset-100 response landing after a reconnect has reset to rows 0-49 would
1297
+ * append rows 100-149 — a 50-row hole — and leave the id set at 100, so every
1298
+ * later page re-requests offset 100 and never advances again. The generation
1299
+ * is captured before the await and rechecked after, so a superseded response
1300
+ * is discarded instead.
1301
+ */
1302
+ this.conversationsGeneration = 0;
1265
1303
  // Minimal in-session accumulation for the assistant message record.
1266
1304
  // Only top-level ``text`` blocks contribute; subagent / tool output
1267
1305
  // is tracked by the consumer's own block store.
@@ -1301,7 +1339,7 @@ var ChatSession = class {
1301
1339
  async connect() {
1302
1340
  const [status, conversations, agents, skills] = await Promise.allSettled([
1303
1341
  this.client.getAgentStatus(),
1304
- this.client.getConversations(),
1342
+ this.client.getConversations(CONVERSATION_PAGE_SIZE),
1305
1343
  this.client.getAgents().catch(() => []),
1306
1344
  this.client.getSkills().catch(() => [])
1307
1345
  ]);
@@ -1309,7 +1347,12 @@ var ChatSession = class {
1309
1347
  this.agentStatus = status.value;
1310
1348
  }
1311
1349
  if (conversations.status === "fulfilled") {
1350
+ this.conversationsGeneration++;
1312
1351
  this.conversations = conversations.value;
1352
+ this.serverConversationIds = new Set(
1353
+ conversations.value.map((c) => c.id)
1354
+ );
1355
+ this.hasMoreConversations = conversations.value.length === CONVERSATION_PAGE_SIZE;
1313
1356
  }
1314
1357
  if (agents.status === "fulfilled") {
1315
1358
  this.agents = agents.value;
@@ -1768,12 +1811,69 @@ var ChatSession = class {
1768
1811
  eventsResult.status === "fulfilled" ? eventsResult.value : []
1769
1812
  );
1770
1813
  }
1814
+ /**
1815
+ * Append the next page of conversation history to ``conversations``.
1816
+ *
1817
+ * The list is ordered ``updated_at DESC`` and paged by offset, so a
1818
+ * conversation bumped to the top mid-scroll can surface again in a later
1819
+ * page; ids already held are dropped rather than duplicated. Returns only
1820
+ * the conversations actually appended, which may be empty even on a full
1821
+ * page. Rejects on network failure with ``hasMoreConversations`` still true,
1822
+ * so the caller can retry.
1823
+ *
1824
+ * KNOWN LIMITATION — offset paging is only stable while the prefix already
1825
+ * consumed stays put. The offset tracking here corrects for perturbations
1826
+ * THIS session causes (local unshifts, ``deleteConversation``), but not for
1827
+ * ones it never sees:
1828
+ *
1829
+ * - a conversation this session hasn't loaded yet is bumped to the top (a
1830
+ * headless routine or another device posting to it), pushing the whole
1831
+ * list down — it lands inside the consumed prefix, which no later offset
1832
+ * revisits;
1833
+ * - a conversation is deleted from another tab/device, shrinking the list so
1834
+ * the next offset lands one row too far in.
1835
+ *
1836
+ * Each perturbation costs at most one conversation off the sidebar, and only
1837
+ * until the next ``connect()`` — that re-seeds page 1 and resets the paging
1838
+ * state, so a reload or reconnect always recovers it. Nothing is lost
1839
+ * server-side. Both cases are pinned by tests in
1840
+ * ``tests/conversation-paging.test.ts``.
1841
+ *
1842
+ * Closing the gap properly needs a stable server cursor (keyset paging on
1843
+ * ``(updated_at, id)``) rather than a raw offset, which is a backend change —
1844
+ * tracking ids client-side cannot discover a row that moved into a region
1845
+ * already scanned.
1846
+ */
1847
+ async loadMoreConversations() {
1848
+ if (this.isLoadingConversations || !this.hasMoreConversations) return [];
1849
+ this.isLoadingConversations = true;
1850
+ const generation = this.conversationsGeneration;
1851
+ try {
1852
+ const page = await this.client.getConversations(
1853
+ CONVERSATION_PAGE_SIZE,
1854
+ this.serverConversationIds.size
1855
+ );
1856
+ if (generation !== this.conversationsGeneration) return [];
1857
+ this.hasMoreConversations = page.length === CONVERSATION_PAGE_SIZE;
1858
+ const fresh = page.filter((c) => !this.serverConversationIds.has(c.id));
1859
+ for (const c of page) this.serverConversationIds.add(c.id);
1860
+ const known = new Set(this.conversations.map((c) => c.id));
1861
+ const appended = fresh.filter((c) => !known.has(c.id));
1862
+ this.conversations.push(...appended);
1863
+ return appended;
1864
+ } finally {
1865
+ this.isLoadingConversations = false;
1866
+ }
1867
+ }
1771
1868
  async deleteConversation(id) {
1772
1869
  try {
1773
1870
  await this.client.deleteConversation(id);
1774
1871
  } catch {
1775
1872
  }
1776
1873
  await this.storage.deleteConversation(id);
1874
+ if (this.serverConversationIds.delete(id)) {
1875
+ this.conversationsGeneration++;
1876
+ }
1777
1877
  this.conversations = this.conversations.filter((c) => c.id !== id);
1778
1878
  if (this.conversationId === id) {
1779
1879
  this.conversationId = null;
@@ -2158,6 +2258,7 @@ export {
2158
2258
  AstralformClient,
2159
2259
  AstralformError,
2160
2260
  AuthenticationError,
2261
+ CONVERSATION_PAGE_SIZE,
2161
2262
  ChatEventType,
2162
2263
  ChatSession,
2163
2264
  ConnectionError,