@astralform/js 3.2.0 → 4.1.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
@@ -637,8 +637,13 @@ interface ChatStreamRequest {
637
637
  resend_from?: string;
638
638
  upload_ids?: string[];
639
639
  agent_name?: string;
640
- enable_search?: boolean;
641
640
  plan_mode?: boolean;
641
+ /**
642
+ * Start a durable long-horizon goal for this run (goal mode). The backend mints
643
+ * an agent_goal from this text and drives the run under a budget until the
644
+ * objective is genuinely complete. A blank/omitted value runs a normal turn.
645
+ */
646
+ goal?: string;
642
647
  /**
643
648
  * Per-request model choice (client-side model selection), wire shape.
644
649
  * `provider` and `model` are paired; omit to reuse the thread's last model.
@@ -723,8 +728,13 @@ interface SendOptions$1 extends ModelChoiceOptions {
723
728
  enabledClientTools?: string[];
724
729
  uploadIds?: string[];
725
730
  agentName?: string;
726
- enableSearch?: boolean;
727
731
  planMode?: boolean;
732
+ /**
733
+ * Start a durable long-horizon goal for this run (goal mode). The text becomes
734
+ * the goal's objective; the backend keeps the agent working under a budget until
735
+ * it's complete. Omit for a normal turn.
736
+ */
737
+ goal?: string;
728
738
  }
729
739
  /**
730
740
  * A selectable model for one of the team's connected providers, from
@@ -750,6 +760,13 @@ interface ConversationAsset {
750
760
  workspacePath?: string;
751
761
  sourceMessageId?: string;
752
762
  agentName?: string;
763
+ /**
764
+ * Freshly-signed download/preview URL. Minted per response by the backend
765
+ * (private `workspaces` bucket), so it reflects the current signature rather
766
+ * than a link baked in when the asset was created. May be absent if signing
767
+ * failed or the asset has no stored object.
768
+ */
769
+ url?: string;
753
770
  createdAt: string;
754
771
  }
755
772
 
@@ -969,9 +986,7 @@ declare class ChatSession {
969
986
  private emit;
970
987
  connect(): Promise<void>;
971
988
  send(content: string, options?: SendOptions$1): Promise<void>;
972
- resendFromCheckpoint(messageId: string, newContent: string, options?: {
973
- enableSearch?: boolean;
974
- }): Promise<void>;
989
+ resendFromCheckpoint(messageId: string, newContent: string): Promise<void>;
975
990
  private resetStreamingState;
976
991
  private processStream;
977
992
  /** Last received sequence number for resumable reconnection */
@@ -1002,6 +1017,14 @@ declare class ChatSession {
1002
1017
  /** POST a client-tool result, retrying transient failures a few times. */
1003
1018
  private submitToolResultWithRetry;
1004
1019
  private dispatchWireEvent;
1020
+ /**
1021
+ * Synchronous replay of a single stored wire event — the side-effect +
1022
+ * translate + emit core of ``dispatchWireEvent`` without the (live-only)
1023
+ * client-tool round-trip. Called in a tight synchronous loop during history
1024
+ * restore so the consumer's per-event store writes batch into ONE render
1025
+ * instead of re-typing the whole conversation event by event.
1026
+ */
1027
+ private replayWireEvent;
1005
1028
  /**
1006
1029
  * State mutations driven by wire events. Kept separate from translation so
1007
1030
  * the pure wire → ChatEvent mapping can live in translate.ts and be reused
@@ -1028,16 +1051,33 @@ declare class ChatSession {
1028
1051
  /** Stop the job and disconnect (explicit user action). */
1029
1052
  disconnect(): void;
1030
1053
  createNewConversation(): Promise<string>;
1031
- switchConversation(id: string, jobId?: string,
1032
1054
  /**
1033
- * User prompt that triggered this job, if known. Emitted as a
1034
- * synthetic ``user_message`` ChatEvent at the START of the replay —
1035
- * a completed job maps to exactly one user turn, so every event in it
1036
- * belongs beneath that prompt. User messages aren't persisted in
1037
- * ``job_events``, so without this the restored conversation would show
1038
- * the agent response with no visible prompt above it.
1055
+ * Replay one completed turn's already-fetched events, synchronously.
1056
+ *
1057
+ * Fetching is the caller's job (``StreamManager.restore`` loads every turn's
1058
+ * events in parallel and the message list once), so this is pure replay: no
1059
+ * awaits, so the whole restore runs in a single synchronous pass and the
1060
+ * consumer batches it into one render.
1061
+ *
1062
+ * ``userMessageContent`` is the prompt that triggered this turn. It's emitted
1063
+ * as a synthetic ``user_message`` BEFORE any of the turn's events: user
1064
+ * prompts aren't persisted in ``job_events``, and some events precede
1065
+ * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1066
+ * so leading with the prompt keeps the turn in order.
1067
+ */
1068
+ replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string): void;
1069
+ /**
1070
+ * Load a conversation's messages and replay its persisted history.
1071
+ *
1072
+ * Convenience for plain-``ChatSession`` consumers (the documented
1073
+ * conversation-management API). ``StreamManager`` drives restore itself —
1074
+ * loading messages once and replaying each turn in parallel — and does NOT
1075
+ * call this; it's kept so direct-Session usage doesn't break.
1076
+ *
1077
+ * Without ``jobId`` it replays the whole conversation; with one, just that
1078
+ * job's events.
1039
1079
  */
1040
- userMessageContent?: string): Promise<void>;
1080
+ switchConversation(id: string, jobId?: string): Promise<void>;
1041
1081
  deleteConversation(id: string): Promise<void>;
1042
1082
  toggleClientTool(name: string): boolean;
1043
1083
  }
@@ -1110,10 +1150,14 @@ declare function streamJobSSE(options: StreamJobSSEOptions): AsyncGenerator<Chat
1110
1150
 
1111
1151
  type StreamState = "idle" | "streaming" | "restoring" | "detached";
1112
1152
  interface SendOptions extends ModelChoiceOptions {
1113
- enableSearch?: boolean;
1114
1153
  agentName?: string;
1115
1154
  uploadIds?: string[];
1116
1155
  planMode?: boolean;
1156
+ /**
1157
+ * Start a durable long-horizon goal for this run (goal mode) — the text is the
1158
+ * goal objective the backend drives to completion. Omit for a normal turn.
1159
+ */
1160
+ goal?: string;
1117
1161
  }
1118
1162
  type StreamManagerEvent = {
1119
1163
  type: "stateChange";
@@ -1153,7 +1197,26 @@ declare class StreamManager {
1153
1197
  private onSessionEvent;
1154
1198
  send(content: string, options?: SendOptions): Promise<void>;
1155
1199
  regenerate(): Promise<void>;
1156
- switchTo(conversationId: string): Promise<void>;
1200
+ /**
1201
+ * Switch the active conversation.
1202
+ *
1203
+ * ``opts.skipHistoryReplay`` is a fast path for consumers that CACHE a
1204
+ * restored conversation's rendered blocks: it moves the active pointer and
1205
+ * loads the message list (needed for send / regenerate context) but skips
1206
+ * the expensive event fetch + replay, and never enters the ``restoring``
1207
+ * state, so a consumer that clears its block view on ``restoring`` keeps
1208
+ * showing the cached history with no flash of a spinner.
1209
+ *
1210
+ * It still confirms there is no live job before skipping: the in-memory
1211
+ * background-job map is empty on a fresh instance (page reload) and blind to
1212
+ * jobs started in another tab/device, so the fast path always asks the server
1213
+ * (``getActiveJob``) and falls through to a full reconnect if one is running.
1214
+ * That one small request is the only cost it doesn't skip, so passing the
1215
+ * flag whenever you hold cached blocks is safe.
1216
+ */
1217
+ switchTo(conversationId: string, opts?: {
1218
+ skipHistoryReplay?: boolean;
1219
+ }): Promise<void>;
1157
1220
  createConversation(): Promise<string>;
1158
1221
  deleteConversation(id: string): Promise<void>;
1159
1222
  stop(): void;
@@ -1200,7 +1263,9 @@ declare function mapSseToChat(raw: RawSseEvent): ChatEvent[];
1200
1263
  * ``message_start`` (e.g. ``memory_recall``, emitted during prompt prep), so
1201
1264
  * gating the user block on ``message_start`` would replay them above the
1202
1265
  * user's own message. Keying off ``job_id`` injects the prompt once per job,
1203
- * before its first event — matching ``session.ts#switchConversation``.
1266
+ * before its first event — matching how the restore path
1267
+ * (``stream-manager.ts#restore`` → ``session.ts#replayTurn``) leads each turn
1268
+ * with its own prompt.
1204
1269
  */
1205
1270
  declare function replayEvents(sseEvents: RawSseEvent[], userMessages: {
1206
1271
  role: string;
package/dist/index.d.ts CHANGED
@@ -637,8 +637,13 @@ interface ChatStreamRequest {
637
637
  resend_from?: string;
638
638
  upload_ids?: string[];
639
639
  agent_name?: string;
640
- enable_search?: boolean;
641
640
  plan_mode?: boolean;
641
+ /**
642
+ * Start a durable long-horizon goal for this run (goal mode). The backend mints
643
+ * an agent_goal from this text and drives the run under a budget until the
644
+ * objective is genuinely complete. A blank/omitted value runs a normal turn.
645
+ */
646
+ goal?: string;
642
647
  /**
643
648
  * Per-request model choice (client-side model selection), wire shape.
644
649
  * `provider` and `model` are paired; omit to reuse the thread's last model.
@@ -723,8 +728,13 @@ interface SendOptions$1 extends ModelChoiceOptions {
723
728
  enabledClientTools?: string[];
724
729
  uploadIds?: string[];
725
730
  agentName?: string;
726
- enableSearch?: boolean;
727
731
  planMode?: boolean;
732
+ /**
733
+ * Start a durable long-horizon goal for this run (goal mode). The text becomes
734
+ * the goal's objective; the backend keeps the agent working under a budget until
735
+ * it's complete. Omit for a normal turn.
736
+ */
737
+ goal?: string;
728
738
  }
729
739
  /**
730
740
  * A selectable model for one of the team's connected providers, from
@@ -750,6 +760,13 @@ interface ConversationAsset {
750
760
  workspacePath?: string;
751
761
  sourceMessageId?: string;
752
762
  agentName?: string;
763
+ /**
764
+ * Freshly-signed download/preview URL. Minted per response by the backend
765
+ * (private `workspaces` bucket), so it reflects the current signature rather
766
+ * than a link baked in when the asset was created. May be absent if signing
767
+ * failed or the asset has no stored object.
768
+ */
769
+ url?: string;
753
770
  createdAt: string;
754
771
  }
755
772
 
@@ -969,9 +986,7 @@ declare class ChatSession {
969
986
  private emit;
970
987
  connect(): Promise<void>;
971
988
  send(content: string, options?: SendOptions$1): Promise<void>;
972
- resendFromCheckpoint(messageId: string, newContent: string, options?: {
973
- enableSearch?: boolean;
974
- }): Promise<void>;
989
+ resendFromCheckpoint(messageId: string, newContent: string): Promise<void>;
975
990
  private resetStreamingState;
976
991
  private processStream;
977
992
  /** Last received sequence number for resumable reconnection */
@@ -1002,6 +1017,14 @@ declare class ChatSession {
1002
1017
  /** POST a client-tool result, retrying transient failures a few times. */
1003
1018
  private submitToolResultWithRetry;
1004
1019
  private dispatchWireEvent;
1020
+ /**
1021
+ * Synchronous replay of a single stored wire event — the side-effect +
1022
+ * translate + emit core of ``dispatchWireEvent`` without the (live-only)
1023
+ * client-tool round-trip. Called in a tight synchronous loop during history
1024
+ * restore so the consumer's per-event store writes batch into ONE render
1025
+ * instead of re-typing the whole conversation event by event.
1026
+ */
1027
+ private replayWireEvent;
1005
1028
  /**
1006
1029
  * State mutations driven by wire events. Kept separate from translation so
1007
1030
  * the pure wire → ChatEvent mapping can live in translate.ts and be reused
@@ -1028,16 +1051,33 @@ declare class ChatSession {
1028
1051
  /** Stop the job and disconnect (explicit user action). */
1029
1052
  disconnect(): void;
1030
1053
  createNewConversation(): Promise<string>;
1031
- switchConversation(id: string, jobId?: string,
1032
1054
  /**
1033
- * User prompt that triggered this job, if known. Emitted as a
1034
- * synthetic ``user_message`` ChatEvent at the START of the replay —
1035
- * a completed job maps to exactly one user turn, so every event in it
1036
- * belongs beneath that prompt. User messages aren't persisted in
1037
- * ``job_events``, so without this the restored conversation would show
1038
- * the agent response with no visible prompt above it.
1055
+ * Replay one completed turn's already-fetched events, synchronously.
1056
+ *
1057
+ * Fetching is the caller's job (``StreamManager.restore`` loads every turn's
1058
+ * events in parallel and the message list once), so this is pure replay: no
1059
+ * awaits, so the whole restore runs in a single synchronous pass and the
1060
+ * consumer batches it into one render.
1061
+ *
1062
+ * ``userMessageContent`` is the prompt that triggered this turn. It's emitted
1063
+ * as a synthetic ``user_message`` BEFORE any of the turn's events: user
1064
+ * prompts aren't persisted in ``job_events``, and some events precede
1065
+ * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1066
+ * so leading with the prompt keeps the turn in order.
1067
+ */
1068
+ replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string): void;
1069
+ /**
1070
+ * Load a conversation's messages and replay its persisted history.
1071
+ *
1072
+ * Convenience for plain-``ChatSession`` consumers (the documented
1073
+ * conversation-management API). ``StreamManager`` drives restore itself —
1074
+ * loading messages once and replaying each turn in parallel — and does NOT
1075
+ * call this; it's kept so direct-Session usage doesn't break.
1076
+ *
1077
+ * Without ``jobId`` it replays the whole conversation; with one, just that
1078
+ * job's events.
1039
1079
  */
1040
- userMessageContent?: string): Promise<void>;
1080
+ switchConversation(id: string, jobId?: string): Promise<void>;
1041
1081
  deleteConversation(id: string): Promise<void>;
1042
1082
  toggleClientTool(name: string): boolean;
1043
1083
  }
@@ -1110,10 +1150,14 @@ declare function streamJobSSE(options: StreamJobSSEOptions): AsyncGenerator<Chat
1110
1150
 
1111
1151
  type StreamState = "idle" | "streaming" | "restoring" | "detached";
1112
1152
  interface SendOptions extends ModelChoiceOptions {
1113
- enableSearch?: boolean;
1114
1153
  agentName?: string;
1115
1154
  uploadIds?: string[];
1116
1155
  planMode?: boolean;
1156
+ /**
1157
+ * Start a durable long-horizon goal for this run (goal mode) — the text is the
1158
+ * goal objective the backend drives to completion. Omit for a normal turn.
1159
+ */
1160
+ goal?: string;
1117
1161
  }
1118
1162
  type StreamManagerEvent = {
1119
1163
  type: "stateChange";
@@ -1153,7 +1197,26 @@ declare class StreamManager {
1153
1197
  private onSessionEvent;
1154
1198
  send(content: string, options?: SendOptions): Promise<void>;
1155
1199
  regenerate(): Promise<void>;
1156
- switchTo(conversationId: string): Promise<void>;
1200
+ /**
1201
+ * Switch the active conversation.
1202
+ *
1203
+ * ``opts.skipHistoryReplay`` is a fast path for consumers that CACHE a
1204
+ * restored conversation's rendered blocks: it moves the active pointer and
1205
+ * loads the message list (needed for send / regenerate context) but skips
1206
+ * the expensive event fetch + replay, and never enters the ``restoring``
1207
+ * state, so a consumer that clears its block view on ``restoring`` keeps
1208
+ * showing the cached history with no flash of a spinner.
1209
+ *
1210
+ * It still confirms there is no live job before skipping: the in-memory
1211
+ * background-job map is empty on a fresh instance (page reload) and blind to
1212
+ * jobs started in another tab/device, so the fast path always asks the server
1213
+ * (``getActiveJob``) and falls through to a full reconnect if one is running.
1214
+ * That one small request is the only cost it doesn't skip, so passing the
1215
+ * flag whenever you hold cached blocks is safe.
1216
+ */
1217
+ switchTo(conversationId: string, opts?: {
1218
+ skipHistoryReplay?: boolean;
1219
+ }): Promise<void>;
1157
1220
  createConversation(): Promise<string>;
1158
1221
  deleteConversation(id: string): Promise<void>;
1159
1222
  stop(): void;
@@ -1200,7 +1263,9 @@ declare function mapSseToChat(raw: RawSseEvent): ChatEvent[];
1200
1263
  * ``message_start`` (e.g. ``memory_recall``, emitted during prompt prep), so
1201
1264
  * gating the user block on ``message_start`` would replay them above the
1202
1265
  * user's own message. Keying off ``job_id`` injects the prompt once per job,
1203
- * before its first event — matching ``session.ts#switchConversation``.
1266
+ * before its first event — matching how the restore path
1267
+ * (``stream-manager.ts#restore`` → ``session.ts#replayTurn``) leads each turn
1268
+ * with its own prompt.
1204
1269
  */
1205
1270
  declare function replayEvents(sseEvents: RawSseEvent[], userMessages: {
1206
1271
  role: string;
package/dist/index.js CHANGED
@@ -592,6 +592,10 @@ var AstralformClient = class {
592
592
  workspacePath: raw.workspace_path,
593
593
  sourceMessageId: raw.source_message_id,
594
594
  agentName: raw.agent_name,
595
+ // The API serializes an unsigned asset as `url: null`; normalize to
596
+ // undefined so it matches the `url?: string` type and consumers that
597
+ // check `!== undefined` never receive a null.
598
+ url: raw.url ?? void 0,
595
599
  createdAt: raw.created_at
596
600
  };
597
601
  }
@@ -1297,8 +1301,8 @@ var ChatSession = class {
1297
1301
  ),
1298
1302
  upload_ids: options?.uploadIds,
1299
1303
  agent_name: options?.agentName,
1300
- enable_search: options?.enableSearch,
1301
1304
  plan_mode: options?.planMode,
1305
+ goal: options?.goal,
1302
1306
  // Per-request model choice (client-side model selection).
1303
1307
  provider: options?.provider,
1304
1308
  model: options?.model,
@@ -1307,15 +1311,14 @@ var ChatSession = class {
1307
1311
  };
1308
1312
  await this.processStream(request);
1309
1313
  }
1310
- async resendFromCheckpoint(messageId, newContent, options) {
1314
+ async resendFromCheckpoint(messageId, newContent) {
1311
1315
  if (this.isStreaming) return;
1312
1316
  const request = {
1313
1317
  message: newContent,
1314
1318
  conversation_id: this.conversationId ?? void 0,
1315
1319
  resend_from: messageId,
1316
1320
  mcp_manifest: this.toolRegistry.getManifest(),
1317
- enabled_mcp: Array.from(this.enabledClientTools),
1318
- enable_search: options?.enableSearch
1321
+ enabled_mcp: Array.from(this.enabledClientTools)
1319
1322
  };
1320
1323
  await this.processStream(request);
1321
1324
  }
@@ -1506,6 +1509,20 @@ var ChatSession = class {
1506
1509
  }
1507
1510
  }
1508
1511
  }
1512
+ /**
1513
+ * Synchronous replay of a single stored wire event — the side-effect +
1514
+ * translate + emit core of ``dispatchWireEvent`` without the (live-only)
1515
+ * client-tool round-trip. Called in a tight synchronous loop during history
1516
+ * restore so the consumer's per-event store writes batch into ONE render
1517
+ * instead of re-typing the whole conversation event by event.
1518
+ */
1519
+ replayWireEvent(wire, conversationId) {
1520
+ this.applyWireSideEffects(wire, conversationId, "");
1521
+ const event = translateWireEvent(wire);
1522
+ if (event) {
1523
+ this.emit(event);
1524
+ }
1525
+ }
1509
1526
  /**
1510
1527
  * State mutations driven by wire events. Kept separate from translation so
1511
1528
  * the pure wire → ChatEvent mapping can live in translate.ts and be reused
@@ -1652,34 +1669,57 @@ var ChatSession = class {
1652
1669
  this.messages = [];
1653
1670
  return id;
1654
1671
  }
1655
- async switchConversation(id, jobId, userMessageContent) {
1672
+ /**
1673
+ * Replay one completed turn's already-fetched events, synchronously.
1674
+ *
1675
+ * Fetching is the caller's job (``StreamManager.restore`` loads every turn's
1676
+ * events in parallel and the message list once), so this is pure replay: no
1677
+ * awaits, so the whole restore runs in a single synchronous pass and the
1678
+ * consumer batches it into one render.
1679
+ *
1680
+ * ``userMessageContent`` is the prompt that triggered this turn. It's emitted
1681
+ * as a synthetic ``user_message`` BEFORE any of the turn's events: user
1682
+ * prompts aren't persisted in ``job_events``, and some events precede
1683
+ * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1684
+ * so leading with the prompt keeps the turn in order.
1685
+ */
1686
+ replayTurn(id, events, userMessageContent) {
1656
1687
  this.conversationId = id;
1657
1688
  this.resetStreamingState();
1689
+ if (userMessageContent) {
1690
+ this.emit({ type: "user_message", content: userMessageContent });
1691
+ }
1692
+ for (const ev of events) {
1693
+ const type = ev.data.type || ev.event;
1694
+ if (!type || type === "done") continue;
1695
+ const wire = { ...ev.data, type };
1696
+ try {
1697
+ this.replayWireEvent(wire, id);
1698
+ } catch {
1699
+ }
1700
+ }
1701
+ }
1702
+ /**
1703
+ * Load a conversation's messages and replay its persisted history.
1704
+ *
1705
+ * Convenience for plain-``ChatSession`` consumers (the documented
1706
+ * conversation-management API). ``StreamManager`` drives restore itself —
1707
+ * loading messages once and replaying each turn in parallel — and does NOT
1708
+ * call this; it's kept so direct-Session usage doesn't break.
1709
+ *
1710
+ * Without ``jobId`` it replays the whole conversation; with one, just that
1711
+ * job's events.
1712
+ */
1713
+ async switchConversation(id, jobId) {
1658
1714
  const [messagesResult, eventsResult] = await Promise.allSettled([
1659
1715
  this.client.getMessages(id).catch(() => this.storage.fetchMessages(id)),
1660
1716
  this.client.getConversationEvents(id, jobId)
1661
1717
  ]);
1662
1718
  this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
1663
- if (eventsResult.status === "fulfilled") {
1664
- if (userMessageContent) {
1665
- this.emit({ type: "user_message", content: userMessageContent });
1666
- }
1667
- for (const ev of eventsResult.value) {
1668
- const type = ev.data.type || ev.event;
1669
- if (!type || type === "done") continue;
1670
- const wire = { ...ev.data, type };
1671
- try {
1672
- await this.dispatchWireEvent(
1673
- wire,
1674
- id,
1675
- "",
1676
- false
1677
- // don't execute client tools on replay
1678
- );
1679
- } catch {
1680
- }
1681
- }
1682
- }
1719
+ this.replayTurn(
1720
+ id,
1721
+ eventsResult.status === "fulfilled" ? eventsResult.value : []
1722
+ );
1683
1723
  }
1684
1724
  async deleteConversation(id) {
1685
1725
  try {
@@ -1823,10 +1863,10 @@ var StreamManager = class {
1823
1863
  this.setState("streaming");
1824
1864
  try {
1825
1865
  await this.session.send(content, {
1826
- enableSearch: options?.enableSearch,
1827
1866
  agentName: options?.agentName,
1828
1867
  uploadIds: options?.uploadIds,
1829
1868
  planMode: options?.planMode,
1869
+ goal: options?.goal,
1830
1870
  provider: options?.provider,
1831
1871
  model: options?.model,
1832
1872
  reasoningEffort: options?.reasoningEffort,
@@ -1855,8 +1895,26 @@ var StreamManager = class {
1855
1895
  this.finalizeStream();
1856
1896
  }
1857
1897
  // ── Switch conversation ───────────────────────────────────────
1858
- async switchTo(conversationId) {
1898
+ /**
1899
+ * Switch the active conversation.
1900
+ *
1901
+ * ``opts.skipHistoryReplay`` is a fast path for consumers that CACHE a
1902
+ * restored conversation's rendered blocks: it moves the active pointer and
1903
+ * loads the message list (needed for send / regenerate context) but skips
1904
+ * the expensive event fetch + replay, and never enters the ``restoring``
1905
+ * state, so a consumer that clears its block view on ``restoring`` keeps
1906
+ * showing the cached history with no flash of a spinner.
1907
+ *
1908
+ * It still confirms there is no live job before skipping: the in-memory
1909
+ * background-job map is empty on a fresh instance (page reload) and blind to
1910
+ * jobs started in another tab/device, so the fast path always asks the server
1911
+ * (``getActiveJob``) and falls through to a full reconnect if one is running.
1912
+ * That one small request is the only cost it doesn't skip, so passing the
1913
+ * flag whenever you hold cached blocks is safe.
1914
+ */
1915
+ async switchTo(conversationId, opts) {
1859
1916
  if (conversationId === this._activeConversationId) return;
1917
+ const targetHadBackgroundJob = this._backgroundJobs.has(conversationId);
1860
1918
  if (this._state === "streaming") {
1861
1919
  const oldConvId = this._activeConversationId;
1862
1920
  const jobId = this.session.currentJobId;
@@ -1877,6 +1935,18 @@ var StreamManager = class {
1877
1935
  });
1878
1936
  }
1879
1937
  this.setActiveConversation(conversationId);
1938
+ if (opts?.skipHistoryReplay && !targetHadBackgroundJob) {
1939
+ let activeJobId = null;
1940
+ try {
1941
+ activeJobId = (await this.session.client.getActiveJob(conversationId)).jobId;
1942
+ } catch {
1943
+ }
1944
+ if (!activeJobId) {
1945
+ await this.session.loadConversation(conversationId);
1946
+ this.setState("idle");
1947
+ return;
1948
+ }
1949
+ }
1880
1950
  await this.restore(conversationId);
1881
1951
  }
1882
1952
  // ── Create / delete conversation ──────────────────────────────
@@ -1941,13 +2011,16 @@ var StreamManager = class {
1941
2011
  const userMessages = this.session.messages.filter(
1942
2012
  (m) => m.role === "user"
1943
2013
  );
2014
+ const eventLists = await Promise.all(
2015
+ completedJobs.map(
2016
+ (job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
2017
+ )
2018
+ );
1944
2019
  for (let i = 0; i < completedJobs.length; i++) {
1945
- const job = completedJobs[i];
1946
- const userContent = userMessages[i]?.content;
1947
- await this.session.switchConversation(
2020
+ this.session.replayTurn(
1948
2021
  conversationId,
1949
- job.job_id,
1950
- userContent
2022
+ eventLists[i] ?? [],
2023
+ userMessages[i]?.content
1951
2024
  );
1952
2025
  }
1953
2026
  if (completedJobs.length > 0) {