@astralform/js 4.2.0 → 4.4.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
@@ -420,6 +420,12 @@ type ChatEvent = {
420
420
  type: "user_message";
421
421
  content: string;
422
422
  createdAt?: number;
423
+ id?: string;
424
+ /** This message was steered into a run already in flight — it started
425
+ * no turn of its own. Consumers that badge a steer (a "waiting to be
426
+ * read" notice) key off this; without it a replayed steer is
427
+ * indistinguishable from an ordinary prompt. */
428
+ steer?: boolean;
423
429
  } | {
424
430
  type: "title_generated";
425
431
  title: string;
@@ -796,7 +802,7 @@ declare class AstralformClient {
796
802
  constructor(config: AstralformConfig);
797
803
  /**
798
804
  * 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).
805
+ * Use after refreshing via the host app's own token manager.
800
806
  * Throws if the client was created in API-key mode.
801
807
  */
802
808
  updateAccessToken(accessToken: string): void;
@@ -980,6 +986,13 @@ declare class ToolRegistry {
980
986
  }
981
987
 
982
988
  type ChatEventHandler = (event: ChatEvent) => void;
989
+ /**
990
+ * Conversations fetched per page, by ``connect`` and ``loadMoreConversations``
991
+ * alike. The two must use the same size: the offset is derived from how many
992
+ * rows the server has returned so far, so a first page of a different size
993
+ * would leave the second page's offset pointing at the wrong row.
994
+ */
995
+ declare const CONVERSATION_PAGE_SIZE = 50;
983
996
  /**
984
997
  * ChatSession — translates the backend wire protocol into typed ChatEvents
985
998
  * for consumers. Owns HTTP + SSE plumbing, conversation state, and the
@@ -1000,6 +1013,17 @@ declare class ChatSession {
1000
1013
  readonly protocols: ProtocolRegistry<ProtocolAdapter>;
1001
1014
  conversationId: string | null;
1002
1015
  conversations: Conversation[];
1016
+ /**
1017
+ * Whether another page of conversations may exist on the server.
1018
+ *
1019
+ * Inferred from the last page being full, since the list endpoint returns a
1020
+ * bare array with no total. A total that happens to be an exact multiple of
1021
+ * the page size therefore costs one extra empty request before this flips —
1022
+ * cheaper than adding a count query to every list call.
1023
+ */
1024
+ hasMoreConversations: boolean;
1025
+ /** True while ``loadMoreConversations`` is in flight. */
1026
+ isLoadingConversations: boolean;
1003
1027
  messages: Message[];
1004
1028
  isStreaming: boolean;
1005
1029
  agentStatus: AgentStatus | null;
@@ -1007,6 +1031,32 @@ declare class ChatSession {
1007
1031
  skills: SkillInfo[];
1008
1032
  enabledClientTools: Set<string>;
1009
1033
  modelDisplayName: string | null;
1034
+ /**
1035
+ * Ids of conversations the SERVER has handed us, which is the paging offset.
1036
+ *
1037
+ * Deliberately not ``conversations.length``. That array also holds
1038
+ * conversations created locally and unshifted on top (``createNewConversation``,
1039
+ * and the auto-created conversation in ``consumeJobStream``), so using its
1040
+ * length as the offset would over-count and silently skip a row of real
1041
+ * history on the next page. Tracking ids rather than a counter also makes
1042
+ * deletion self-correcting: removing a server-sourced conversation shifts
1043
+ * every later page up by one, and dropping its id from this set is exactly
1044
+ * that shift — while deleting a purely local one correctly changes nothing.
1045
+ */
1046
+ private serverConversationIds;
1047
+ /**
1048
+ * Bumped every time ``connect()`` re-seeds the conversation list.
1049
+ *
1050
+ * A ``loadMoreConversations`` request issued before a re-seed describes the
1051
+ * OLD paging state, so applying its response afterwards both appends the
1052
+ * wrong rows and corrupts the offset. Concretely: with 100 rows held, an
1053
+ * offset-100 response landing after a reconnect has reset to rows 0-49 would
1054
+ * append rows 100-149 — a 50-row hole — and leave the id set at 100, so every
1055
+ * later page re-requests offset 100 and never advances again. The generation
1056
+ * is captured before the await and rechecked after, so a superseded response
1057
+ * is discarded instead.
1058
+ */
1059
+ private conversationsGeneration;
1010
1060
  private accumulatedText;
1011
1061
  private currentTextPath;
1012
1062
  private handlers;
@@ -1095,7 +1145,7 @@ declare class ChatSession {
1095
1145
  * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1096
1146
  * so leading with the prompt keeps the turn in order.
1097
1147
  */
1098
- replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string): void;
1148
+ replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string, userMessageId?: string, isSteer?: boolean): void;
1099
1149
  /**
1100
1150
  * Load a conversation's messages and replay its persisted history.
1101
1151
  *
@@ -1108,6 +1158,40 @@ declare class ChatSession {
1108
1158
  * job's events.
1109
1159
  */
1110
1160
  switchConversation(id: string, jobId?: string): Promise<void>;
1161
+ /**
1162
+ * Append the next page of conversation history to ``conversations``.
1163
+ *
1164
+ * The list is ordered ``updated_at DESC`` and paged by offset, so a
1165
+ * conversation bumped to the top mid-scroll can surface again in a later
1166
+ * page; ids already held are dropped rather than duplicated. Returns only
1167
+ * the conversations actually appended, which may be empty even on a full
1168
+ * page. Rejects on network failure with ``hasMoreConversations`` still true,
1169
+ * so the caller can retry.
1170
+ *
1171
+ * KNOWN LIMITATION — offset paging is only stable while the prefix already
1172
+ * consumed stays put. The offset tracking here corrects for perturbations
1173
+ * THIS session causes (local unshifts, ``deleteConversation``), but not for
1174
+ * ones it never sees:
1175
+ *
1176
+ * - a conversation this session hasn't loaded yet is bumped to the top (a
1177
+ * headless routine or another device posting to it), pushing the whole
1178
+ * list down — it lands inside the consumed prefix, which no later offset
1179
+ * revisits;
1180
+ * - a conversation is deleted from another tab/device, shrinking the list so
1181
+ * the next offset lands one row too far in.
1182
+ *
1183
+ * Each perturbation costs at most one conversation off the sidebar, and only
1184
+ * until the next ``connect()`` — that re-seeds page 1 and resets the paging
1185
+ * state, so a reload or reconnect always recovers it. Nothing is lost
1186
+ * server-side. Both cases are pinned by tests in
1187
+ * ``tests/conversation-paging.test.ts``.
1188
+ *
1189
+ * Closing the gap properly needs a stable server cursor (keyset paging on
1190
+ * ``(updated_at, id)``) rather than a raw offset, which is a backend change —
1191
+ * tracking ids client-side cannot discover a row that moved into a region
1192
+ * already scanned.
1193
+ */
1194
+ loadMoreConversations(): Promise<Conversation[]>;
1111
1195
  deleteConversation(id: string): Promise<void>;
1112
1196
  toggleClientTool(name: string): boolean;
1113
1197
  }
@@ -1158,26 +1242,6 @@ declare function generateId(): string;
1158
1242
  */
1159
1243
  declare function streamJobSSE(options: StreamJobSSEOptions): AsyncGenerator<ChatStreamEvent>;
1160
1244
 
1161
- /**
1162
- * StreamManager — high-level conversation lifecycle coordinator.
1163
- *
1164
- * Sits on top of ChatSession and manages the state machine for
1165
- * multi-conversation SSE streaming. Framework-agnostic: emits
1166
- * typed events to registered handlers. Block construction is NOT
1167
- * the SDK's concern — consumers build their own block tree from
1168
- * the forwarded ``ChatEvent`` instances.
1169
- *
1170
- * import { ChatSession, StreamManager } from "@astralform/js";
1171
- * const session = new ChatSession({ ... });
1172
- * const manager = new StreamManager(session);
1173
- * manager.on((event) => {
1174
- * if (event.type === "event") {
1175
- * // event.event is a typed ChatEvent — dispatch to your reducer
1176
- * }
1177
- * });
1178
- * await manager.send("Hello");
1179
- */
1180
-
1181
1245
  type StreamState = "idle" | "streaming" | "restoring" | "detached";
1182
1246
  interface SendOptions extends ModelChoiceOptions {
1183
1247
  agentName?: string;
@@ -1340,4 +1404,4 @@ declare function isEmbeddedResource(value: unknown): value is {
1340
1404
  */
1341
1405
  declare function parseEmbeddedResource(value: unknown): EmbeddedResource | null;
1342
1406
 
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 };
1407
+ 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
@@ -420,6 +420,12 @@ type ChatEvent = {
420
420
  type: "user_message";
421
421
  content: string;
422
422
  createdAt?: number;
423
+ id?: string;
424
+ /** This message was steered into a run already in flight — it started
425
+ * no turn of its own. Consumers that badge a steer (a "waiting to be
426
+ * read" notice) key off this; without it a replayed steer is
427
+ * indistinguishable from an ordinary prompt. */
428
+ steer?: boolean;
423
429
  } | {
424
430
  type: "title_generated";
425
431
  title: string;
@@ -796,7 +802,7 @@ declare class AstralformClient {
796
802
  constructor(config: AstralformConfig);
797
803
  /**
798
804
  * 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).
805
+ * Use after refreshing via the host app's own token manager.
800
806
  * Throws if the client was created in API-key mode.
801
807
  */
802
808
  updateAccessToken(accessToken: string): void;
@@ -980,6 +986,13 @@ declare class ToolRegistry {
980
986
  }
981
987
 
982
988
  type ChatEventHandler = (event: ChatEvent) => void;
989
+ /**
990
+ * Conversations fetched per page, by ``connect`` and ``loadMoreConversations``
991
+ * alike. The two must use the same size: the offset is derived from how many
992
+ * rows the server has returned so far, so a first page of a different size
993
+ * would leave the second page's offset pointing at the wrong row.
994
+ */
995
+ declare const CONVERSATION_PAGE_SIZE = 50;
983
996
  /**
984
997
  * ChatSession — translates the backend wire protocol into typed ChatEvents
985
998
  * for consumers. Owns HTTP + SSE plumbing, conversation state, and the
@@ -1000,6 +1013,17 @@ declare class ChatSession {
1000
1013
  readonly protocols: ProtocolRegistry<ProtocolAdapter>;
1001
1014
  conversationId: string | null;
1002
1015
  conversations: Conversation[];
1016
+ /**
1017
+ * Whether another page of conversations may exist on the server.
1018
+ *
1019
+ * Inferred from the last page being full, since the list endpoint returns a
1020
+ * bare array with no total. A total that happens to be an exact multiple of
1021
+ * the page size therefore costs one extra empty request before this flips —
1022
+ * cheaper than adding a count query to every list call.
1023
+ */
1024
+ hasMoreConversations: boolean;
1025
+ /** True while ``loadMoreConversations`` is in flight. */
1026
+ isLoadingConversations: boolean;
1003
1027
  messages: Message[];
1004
1028
  isStreaming: boolean;
1005
1029
  agentStatus: AgentStatus | null;
@@ -1007,6 +1031,32 @@ declare class ChatSession {
1007
1031
  skills: SkillInfo[];
1008
1032
  enabledClientTools: Set<string>;
1009
1033
  modelDisplayName: string | null;
1034
+ /**
1035
+ * Ids of conversations the SERVER has handed us, which is the paging offset.
1036
+ *
1037
+ * Deliberately not ``conversations.length``. That array also holds
1038
+ * conversations created locally and unshifted on top (``createNewConversation``,
1039
+ * and the auto-created conversation in ``consumeJobStream``), so using its
1040
+ * length as the offset would over-count and silently skip a row of real
1041
+ * history on the next page. Tracking ids rather than a counter also makes
1042
+ * deletion self-correcting: removing a server-sourced conversation shifts
1043
+ * every later page up by one, and dropping its id from this set is exactly
1044
+ * that shift — while deleting a purely local one correctly changes nothing.
1045
+ */
1046
+ private serverConversationIds;
1047
+ /**
1048
+ * Bumped every time ``connect()`` re-seeds the conversation list.
1049
+ *
1050
+ * A ``loadMoreConversations`` request issued before a re-seed describes the
1051
+ * OLD paging state, so applying its response afterwards both appends the
1052
+ * wrong rows and corrupts the offset. Concretely: with 100 rows held, an
1053
+ * offset-100 response landing after a reconnect has reset to rows 0-49 would
1054
+ * append rows 100-149 — a 50-row hole — and leave the id set at 100, so every
1055
+ * later page re-requests offset 100 and never advances again. The generation
1056
+ * is captured before the await and rechecked after, so a superseded response
1057
+ * is discarded instead.
1058
+ */
1059
+ private conversationsGeneration;
1010
1060
  private accumulatedText;
1011
1061
  private currentTextPath;
1012
1062
  private handlers;
@@ -1095,7 +1145,7 @@ declare class ChatSession {
1095
1145
  * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1096
1146
  * so leading with the prompt keeps the turn in order.
1097
1147
  */
1098
- replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string): void;
1148
+ replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string, userMessageId?: string, isSteer?: boolean): void;
1099
1149
  /**
1100
1150
  * Load a conversation's messages and replay its persisted history.
1101
1151
  *
@@ -1108,6 +1158,40 @@ declare class ChatSession {
1108
1158
  * job's events.
1109
1159
  */
1110
1160
  switchConversation(id: string, jobId?: string): Promise<void>;
1161
+ /**
1162
+ * Append the next page of conversation history to ``conversations``.
1163
+ *
1164
+ * The list is ordered ``updated_at DESC`` and paged by offset, so a
1165
+ * conversation bumped to the top mid-scroll can surface again in a later
1166
+ * page; ids already held are dropped rather than duplicated. Returns only
1167
+ * the conversations actually appended, which may be empty even on a full
1168
+ * page. Rejects on network failure with ``hasMoreConversations`` still true,
1169
+ * so the caller can retry.
1170
+ *
1171
+ * KNOWN LIMITATION — offset paging is only stable while the prefix already
1172
+ * consumed stays put. The offset tracking here corrects for perturbations
1173
+ * THIS session causes (local unshifts, ``deleteConversation``), but not for
1174
+ * ones it never sees:
1175
+ *
1176
+ * - a conversation this session hasn't loaded yet is bumped to the top (a
1177
+ * headless routine or another device posting to it), pushing the whole
1178
+ * list down — it lands inside the consumed prefix, which no later offset
1179
+ * revisits;
1180
+ * - a conversation is deleted from another tab/device, shrinking the list so
1181
+ * the next offset lands one row too far in.
1182
+ *
1183
+ * Each perturbation costs at most one conversation off the sidebar, and only
1184
+ * until the next ``connect()`` — that re-seeds page 1 and resets the paging
1185
+ * state, so a reload or reconnect always recovers it. Nothing is lost
1186
+ * server-side. Both cases are pinned by tests in
1187
+ * ``tests/conversation-paging.test.ts``.
1188
+ *
1189
+ * Closing the gap properly needs a stable server cursor (keyset paging on
1190
+ * ``(updated_at, id)``) rather than a raw offset, which is a backend change —
1191
+ * tracking ids client-side cannot discover a row that moved into a region
1192
+ * already scanned.
1193
+ */
1194
+ loadMoreConversations(): Promise<Conversation[]>;
1111
1195
  deleteConversation(id: string): Promise<void>;
1112
1196
  toggleClientTool(name: string): boolean;
1113
1197
  }
@@ -1158,26 +1242,6 @@ declare function generateId(): string;
1158
1242
  */
1159
1243
  declare function streamJobSSE(options: StreamJobSSEOptions): AsyncGenerator<ChatStreamEvent>;
1160
1244
 
1161
- /**
1162
- * StreamManager — high-level conversation lifecycle coordinator.
1163
- *
1164
- * Sits on top of ChatSession and manages the state machine for
1165
- * multi-conversation SSE streaming. Framework-agnostic: emits
1166
- * typed events to registered handlers. Block construction is NOT
1167
- * the SDK's concern — consumers build their own block tree from
1168
- * the forwarded ``ChatEvent`` instances.
1169
- *
1170
- * import { ChatSession, StreamManager } from "@astralform/js";
1171
- * const session = new ChatSession({ ... });
1172
- * const manager = new StreamManager(session);
1173
- * manager.on((event) => {
1174
- * if (event.type === "event") {
1175
- * // event.event is a typed ChatEvent — dispatch to your reducer
1176
- * }
1177
- * });
1178
- * await manager.send("Hello");
1179
- */
1180
-
1181
1245
  type StreamState = "idle" | "streaming" | "restoring" | "detached";
1182
1246
  interface SendOptions extends ModelChoiceOptions {
1183
1247
  agentName?: string;
@@ -1340,4 +1404,4 @@ declare function isEmbeddedResource(value: unknown): value is {
1340
1404
  */
1341
1405
  declare function parseEmbeddedResource(value: unknown): EmbeddedResource | null;
1342
1406
 
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 };
1407
+ 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;
@@ -1730,11 +1773,16 @@ var ChatSession = class {
1730
1773
  * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1731
1774
  * so leading with the prompt keeps the turn in order.
1732
1775
  */
1733
- replayTurn(id, events, userMessageContent) {
1776
+ replayTurn(id, events, userMessageContent, userMessageId, isSteer = false) {
1734
1777
  this.conversationId = id;
1735
1778
  this.resetStreamingState();
1736
1779
  if (userMessageContent) {
1737
- this.emit({ type: "user_message", content: userMessageContent });
1780
+ this.emit({
1781
+ type: "user_message",
1782
+ content: userMessageContent,
1783
+ ...userMessageId ? { id: userMessageId } : {},
1784
+ ...isSteer ? { steer: true } : {}
1785
+ });
1738
1786
  }
1739
1787
  for (const ev of events) {
1740
1788
  const type = ev.data.type || ev.event;
@@ -1768,12 +1816,69 @@ var ChatSession = class {
1768
1816
  eventsResult.status === "fulfilled" ? eventsResult.value : []
1769
1817
  );
1770
1818
  }
1819
+ /**
1820
+ * Append the next page of conversation history to ``conversations``.
1821
+ *
1822
+ * The list is ordered ``updated_at DESC`` and paged by offset, so a
1823
+ * conversation bumped to the top mid-scroll can surface again in a later
1824
+ * page; ids already held are dropped rather than duplicated. Returns only
1825
+ * the conversations actually appended, which may be empty even on a full
1826
+ * page. Rejects on network failure with ``hasMoreConversations`` still true,
1827
+ * so the caller can retry.
1828
+ *
1829
+ * KNOWN LIMITATION — offset paging is only stable while the prefix already
1830
+ * consumed stays put. The offset tracking here corrects for perturbations
1831
+ * THIS session causes (local unshifts, ``deleteConversation``), but not for
1832
+ * ones it never sees:
1833
+ *
1834
+ * - a conversation this session hasn't loaded yet is bumped to the top (a
1835
+ * headless routine or another device posting to it), pushing the whole
1836
+ * list down — it lands inside the consumed prefix, which no later offset
1837
+ * revisits;
1838
+ * - a conversation is deleted from another tab/device, shrinking the list so
1839
+ * the next offset lands one row too far in.
1840
+ *
1841
+ * Each perturbation costs at most one conversation off the sidebar, and only
1842
+ * until the next ``connect()`` — that re-seeds page 1 and resets the paging
1843
+ * state, so a reload or reconnect always recovers it. Nothing is lost
1844
+ * server-side. Both cases are pinned by tests in
1845
+ * ``tests/conversation-paging.test.ts``.
1846
+ *
1847
+ * Closing the gap properly needs a stable server cursor (keyset paging on
1848
+ * ``(updated_at, id)``) rather than a raw offset, which is a backend change —
1849
+ * tracking ids client-side cannot discover a row that moved into a region
1850
+ * already scanned.
1851
+ */
1852
+ async loadMoreConversations() {
1853
+ if (this.isLoadingConversations || !this.hasMoreConversations) return [];
1854
+ this.isLoadingConversations = true;
1855
+ const generation = this.conversationsGeneration;
1856
+ try {
1857
+ const page = await this.client.getConversations(
1858
+ CONVERSATION_PAGE_SIZE,
1859
+ this.serverConversationIds.size
1860
+ );
1861
+ if (generation !== this.conversationsGeneration) return [];
1862
+ this.hasMoreConversations = page.length === CONVERSATION_PAGE_SIZE;
1863
+ const fresh = page.filter((c) => !this.serverConversationIds.has(c.id));
1864
+ for (const c of page) this.serverConversationIds.add(c.id);
1865
+ const known = new Set(this.conversations.map((c) => c.id));
1866
+ const appended = fresh.filter((c) => !known.has(c.id));
1867
+ this.conversations.push(...appended);
1868
+ return appended;
1869
+ } finally {
1870
+ this.isLoadingConversations = false;
1871
+ }
1872
+ }
1771
1873
  async deleteConversation(id) {
1772
1874
  try {
1773
1875
  await this.client.deleteConversation(id);
1774
1876
  } catch {
1775
1877
  }
1776
1878
  await this.storage.deleteConversation(id);
1879
+ if (this.serverConversationIds.delete(id)) {
1880
+ this.conversationsGeneration++;
1881
+ }
1777
1882
  this.conversations = this.conversations.filter((c) => c.id !== id);
1778
1883
  if (this.conversationId === id) {
1779
1884
  this.conversationId = null;
@@ -1790,6 +1895,64 @@ var ChatSession = class {
1790
1895
  }
1791
1896
  };
1792
1897
 
1898
+ // src/restore-plan.ts
1899
+ function planRestore(args) {
1900
+ const { completedJobs, userMessages } = args;
1901
+ const byId = /* @__PURE__ */ new Map();
1902
+ userMessages.forEach((m, i) => {
1903
+ if (m.id) byId.set(m.id, i);
1904
+ });
1905
+ const linkOf = (j) => j.message_id ? byId.get(j.message_id) : void 0;
1906
+ const positional = (jobs, msgs) => jobs.map((job, i) => ({
1907
+ kind: "turn",
1908
+ jobId: job.job_id,
1909
+ content: msgs[i]?.content,
1910
+ messageId: msgs[i]?.id
1911
+ }));
1912
+ const firstLinked = completedJobs.findIndex((j) => linkOf(j) !== void 0);
1913
+ if (firstLinked === -1) {
1914
+ return positional(completedJobs, userMessages);
1915
+ }
1916
+ const cutover = linkOf(completedJobs[firstLinked]);
1917
+ const steps = positional(
1918
+ completedJobs.slice(0, firstLinked),
1919
+ userMessages.slice(0, cutover)
1920
+ );
1921
+ let cursor = cutover;
1922
+ const isSteer = (m) => !!m?.id && !completedJobs.some((j) => j.message_id === m.id);
1923
+ const drainTo = (stopAt) => {
1924
+ while (cursor < stopAt) {
1925
+ const m = userMessages[cursor++];
1926
+ if (isSteer(m)) {
1927
+ steps.push({ kind: "steer", content: m.content, messageId: m.id });
1928
+ }
1929
+ }
1930
+ cursor = stopAt + 1;
1931
+ };
1932
+ for (const job of completedJobs.slice(firstLinked)) {
1933
+ const at = linkOf(job);
1934
+ if (at !== void 0) {
1935
+ drainTo(at);
1936
+ const prompt = userMessages[at];
1937
+ steps.push({
1938
+ kind: "turn",
1939
+ jobId: job.job_id,
1940
+ content: prompt.content,
1941
+ messageId: prompt.id
1942
+ });
1943
+ continue;
1944
+ }
1945
+ steps.push({ kind: "turn", jobId: job.job_id });
1946
+ }
1947
+ for (let i = cursor; i < userMessages.length; i++) {
1948
+ const m = userMessages[i];
1949
+ if (isSteer(m)) {
1950
+ steps.push({ kind: "steer", content: m.content, messageId: m.id });
1951
+ }
1952
+ }
1953
+ return steps;
1954
+ }
1955
+
1793
1956
  // src/types.ts
1794
1957
  var ChatEventType = {
1795
1958
  // Connection lifecycle (SDK-local, not wire)
@@ -2058,16 +2221,40 @@ var StreamManager = class {
2058
2221
  const userMessages = this.session.messages.filter(
2059
2222
  (m) => m.role === "user"
2060
2223
  );
2224
+ const plan = planRestore({
2225
+ completedJobs: completedJobs.map((j) => ({
2226
+ job_id: j.job_id,
2227
+ message_id: j.message_id
2228
+ })),
2229
+ userMessages: userMessages.map((m) => ({
2230
+ id: m.id,
2231
+ content: m.content
2232
+ }))
2233
+ });
2061
2234
  const eventLists = await Promise.all(
2062
2235
  completedJobs.map(
2063
2236
  (job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
2064
2237
  )
2065
2238
  );
2066
- for (let i = 0; i < completedJobs.length; i++) {
2239
+ const eventsByJobId = new Map(
2240
+ completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
2241
+ );
2242
+ for (const step of plan) {
2243
+ if (step.kind === "steer") {
2244
+ this.session.replayTurn(
2245
+ conversationId,
2246
+ [],
2247
+ step.content,
2248
+ step.messageId,
2249
+ true
2250
+ );
2251
+ continue;
2252
+ }
2067
2253
  this.session.replayTurn(
2068
2254
  conversationId,
2069
- eventLists[i] ?? [],
2070
- userMessages[i]?.content
2255
+ eventsByJobId.get(step.jobId) ?? [],
2256
+ step.content,
2257
+ step.messageId
2071
2258
  );
2072
2259
  }
2073
2260
  if (completedJobs.length > 0) {
@@ -2158,6 +2345,7 @@ export {
2158
2345
  AstralformClient,
2159
2346
  AstralformError,
2160
2347
  AuthenticationError,
2348
+ CONVERSATION_PAGE_SIZE,
2161
2349
  ChatEventType,
2162
2350
  ChatSession,
2163
2351
  ConnectionError,