@astralform/js 3.1.0 → 4.0.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,7 +637,6 @@ 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;
642
641
  /**
643
642
  * Per-request model choice (client-side model selection), wire shape.
@@ -723,7 +722,6 @@ interface SendOptions$1 extends ModelChoiceOptions {
723
722
  enabledClientTools?: string[];
724
723
  uploadIds?: string[];
725
724
  agentName?: string;
726
- enableSearch?: boolean;
727
725
  planMode?: boolean;
728
726
  }
729
727
  /**
@@ -738,6 +736,8 @@ interface ModelOption {
738
736
  tools: boolean;
739
737
  vision: boolean;
740
738
  thinkingMode: string;
739
+ /** whether the model accepts a configurable reasoning effort (low/medium/high); false for always-on-thinking / think-tags models where effort is a no-op */
740
+ supportsEffort: boolean;
741
741
  }
742
742
  interface ConversationAsset {
743
743
  id: string;
@@ -748,6 +748,13 @@ interface ConversationAsset {
748
748
  workspacePath?: string;
749
749
  sourceMessageId?: string;
750
750
  agentName?: string;
751
+ /**
752
+ * Freshly-signed download/preview URL. Minted per response by the backend
753
+ * (private `workspaces` bucket), so it reflects the current signature rather
754
+ * than a link baked in when the asset was created. May be absent if signing
755
+ * failed or the asset has no stored object.
756
+ */
757
+ url?: string;
751
758
  createdAt: string;
752
759
  }
753
760
 
@@ -967,9 +974,7 @@ declare class ChatSession {
967
974
  private emit;
968
975
  connect(): Promise<void>;
969
976
  send(content: string, options?: SendOptions$1): Promise<void>;
970
- resendFromCheckpoint(messageId: string, newContent: string, options?: {
971
- enableSearch?: boolean;
972
- }): Promise<void>;
977
+ resendFromCheckpoint(messageId: string, newContent: string): Promise<void>;
973
978
  private resetStreamingState;
974
979
  private processStream;
975
980
  /** Last received sequence number for resumable reconnection */
@@ -1000,6 +1005,14 @@ declare class ChatSession {
1000
1005
  /** POST a client-tool result, retrying transient failures a few times. */
1001
1006
  private submitToolResultWithRetry;
1002
1007
  private dispatchWireEvent;
1008
+ /**
1009
+ * Synchronous replay of a single stored wire event — the side-effect +
1010
+ * translate + emit core of ``dispatchWireEvent`` without the (live-only)
1011
+ * client-tool round-trip. Called in a tight synchronous loop during history
1012
+ * restore so the consumer's per-event store writes batch into ONE render
1013
+ * instead of re-typing the whole conversation event by event.
1014
+ */
1015
+ private replayWireEvent;
1003
1016
  /**
1004
1017
  * State mutations driven by wire events. Kept separate from translation so
1005
1018
  * the pure wire → ChatEvent mapping can live in translate.ts and be reused
@@ -1026,16 +1039,33 @@ declare class ChatSession {
1026
1039
  /** Stop the job and disconnect (explicit user action). */
1027
1040
  disconnect(): void;
1028
1041
  createNewConversation(): Promise<string>;
1029
- switchConversation(id: string, jobId?: string,
1030
1042
  /**
1031
- * User prompt that triggered this job, if known. Emitted as a
1032
- * synthetic ``user_message`` ChatEvent at the START of the replay —
1033
- * a completed job maps to exactly one user turn, so every event in it
1034
- * belongs beneath that prompt. User messages aren't persisted in
1035
- * ``job_events``, so without this the restored conversation would show
1036
- * the agent response with no visible prompt above it.
1043
+ * Replay one completed turn's already-fetched events, synchronously.
1044
+ *
1045
+ * Fetching is the caller's job (``StreamManager.restore`` loads every turn's
1046
+ * events in parallel and the message list once), so this is pure replay: no
1047
+ * awaits, so the whole restore runs in a single synchronous pass and the
1048
+ * consumer batches it into one render.
1049
+ *
1050
+ * ``userMessageContent`` is the prompt that triggered this turn. It's emitted
1051
+ * as a synthetic ``user_message`` BEFORE any of the turn's events: user
1052
+ * prompts aren't persisted in ``job_events``, and some events precede
1053
+ * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1054
+ * so leading with the prompt keeps the turn in order.
1055
+ */
1056
+ replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string): void;
1057
+ /**
1058
+ * Load a conversation's messages and replay its persisted history.
1059
+ *
1060
+ * Convenience for plain-``ChatSession`` consumers (the documented
1061
+ * conversation-management API). ``StreamManager`` drives restore itself —
1062
+ * loading messages once and replaying each turn in parallel — and does NOT
1063
+ * call this; it's kept so direct-Session usage doesn't break.
1064
+ *
1065
+ * Without ``jobId`` it replays the whole conversation; with one, just that
1066
+ * job's events.
1037
1067
  */
1038
- userMessageContent?: string): Promise<void>;
1068
+ switchConversation(id: string, jobId?: string): Promise<void>;
1039
1069
  deleteConversation(id: string): Promise<void>;
1040
1070
  toggleClientTool(name: string): boolean;
1041
1071
  }
@@ -1108,7 +1138,6 @@ declare function streamJobSSE(options: StreamJobSSEOptions): AsyncGenerator<Chat
1108
1138
 
1109
1139
  type StreamState = "idle" | "streaming" | "restoring" | "detached";
1110
1140
  interface SendOptions extends ModelChoiceOptions {
1111
- enableSearch?: boolean;
1112
1141
  agentName?: string;
1113
1142
  uploadIds?: string[];
1114
1143
  planMode?: boolean;
@@ -1151,7 +1180,26 @@ declare class StreamManager {
1151
1180
  private onSessionEvent;
1152
1181
  send(content: string, options?: SendOptions): Promise<void>;
1153
1182
  regenerate(): Promise<void>;
1154
- switchTo(conversationId: string): Promise<void>;
1183
+ /**
1184
+ * Switch the active conversation.
1185
+ *
1186
+ * ``opts.skipHistoryReplay`` is a fast path for consumers that CACHE a
1187
+ * restored conversation's rendered blocks: it moves the active pointer and
1188
+ * loads the message list (needed for send / regenerate context) but skips
1189
+ * the expensive event fetch + replay, and never enters the ``restoring``
1190
+ * state, so a consumer that clears its block view on ``restoring`` keeps
1191
+ * showing the cached history with no flash of a spinner.
1192
+ *
1193
+ * It still confirms there is no live job before skipping: the in-memory
1194
+ * background-job map is empty on a fresh instance (page reload) and blind to
1195
+ * jobs started in another tab/device, so the fast path always asks the server
1196
+ * (``getActiveJob``) and falls through to a full reconnect if one is running.
1197
+ * That one small request is the only cost it doesn't skip, so passing the
1198
+ * flag whenever you hold cached blocks is safe.
1199
+ */
1200
+ switchTo(conversationId: string, opts?: {
1201
+ skipHistoryReplay?: boolean;
1202
+ }): Promise<void>;
1155
1203
  createConversation(): Promise<string>;
1156
1204
  deleteConversation(id: string): Promise<void>;
1157
1205
  stop(): void;
@@ -1198,7 +1246,9 @@ declare function mapSseToChat(raw: RawSseEvent): ChatEvent[];
1198
1246
  * ``message_start`` (e.g. ``memory_recall``, emitted during prompt prep), so
1199
1247
  * gating the user block on ``message_start`` would replay them above the
1200
1248
  * user's own message. Keying off ``job_id`` injects the prompt once per job,
1201
- * before its first event — matching ``session.ts#switchConversation``.
1249
+ * before its first event — matching how the restore path
1250
+ * (``stream-manager.ts#restore`` → ``session.ts#replayTurn``) leads each turn
1251
+ * with its own prompt.
1202
1252
  */
1203
1253
  declare function replayEvents(sseEvents: RawSseEvent[], userMessages: {
1204
1254
  role: string;
package/dist/index.d.ts CHANGED
@@ -637,7 +637,6 @@ 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;
642
641
  /**
643
642
  * Per-request model choice (client-side model selection), wire shape.
@@ -723,7 +722,6 @@ interface SendOptions$1 extends ModelChoiceOptions {
723
722
  enabledClientTools?: string[];
724
723
  uploadIds?: string[];
725
724
  agentName?: string;
726
- enableSearch?: boolean;
727
725
  planMode?: boolean;
728
726
  }
729
727
  /**
@@ -738,6 +736,8 @@ interface ModelOption {
738
736
  tools: boolean;
739
737
  vision: boolean;
740
738
  thinkingMode: string;
739
+ /** whether the model accepts a configurable reasoning effort (low/medium/high); false for always-on-thinking / think-tags models where effort is a no-op */
740
+ supportsEffort: boolean;
741
741
  }
742
742
  interface ConversationAsset {
743
743
  id: string;
@@ -748,6 +748,13 @@ interface ConversationAsset {
748
748
  workspacePath?: string;
749
749
  sourceMessageId?: string;
750
750
  agentName?: string;
751
+ /**
752
+ * Freshly-signed download/preview URL. Minted per response by the backend
753
+ * (private `workspaces` bucket), so it reflects the current signature rather
754
+ * than a link baked in when the asset was created. May be absent if signing
755
+ * failed or the asset has no stored object.
756
+ */
757
+ url?: string;
751
758
  createdAt: string;
752
759
  }
753
760
 
@@ -967,9 +974,7 @@ declare class ChatSession {
967
974
  private emit;
968
975
  connect(): Promise<void>;
969
976
  send(content: string, options?: SendOptions$1): Promise<void>;
970
- resendFromCheckpoint(messageId: string, newContent: string, options?: {
971
- enableSearch?: boolean;
972
- }): Promise<void>;
977
+ resendFromCheckpoint(messageId: string, newContent: string): Promise<void>;
973
978
  private resetStreamingState;
974
979
  private processStream;
975
980
  /** Last received sequence number for resumable reconnection */
@@ -1000,6 +1005,14 @@ declare class ChatSession {
1000
1005
  /** POST a client-tool result, retrying transient failures a few times. */
1001
1006
  private submitToolResultWithRetry;
1002
1007
  private dispatchWireEvent;
1008
+ /**
1009
+ * Synchronous replay of a single stored wire event — the side-effect +
1010
+ * translate + emit core of ``dispatchWireEvent`` without the (live-only)
1011
+ * client-tool round-trip. Called in a tight synchronous loop during history
1012
+ * restore so the consumer's per-event store writes batch into ONE render
1013
+ * instead of re-typing the whole conversation event by event.
1014
+ */
1015
+ private replayWireEvent;
1003
1016
  /**
1004
1017
  * State mutations driven by wire events. Kept separate from translation so
1005
1018
  * the pure wire → ChatEvent mapping can live in translate.ts and be reused
@@ -1026,16 +1039,33 @@ declare class ChatSession {
1026
1039
  /** Stop the job and disconnect (explicit user action). */
1027
1040
  disconnect(): void;
1028
1041
  createNewConversation(): Promise<string>;
1029
- switchConversation(id: string, jobId?: string,
1030
1042
  /**
1031
- * User prompt that triggered this job, if known. Emitted as a
1032
- * synthetic ``user_message`` ChatEvent at the START of the replay —
1033
- * a completed job maps to exactly one user turn, so every event in it
1034
- * belongs beneath that prompt. User messages aren't persisted in
1035
- * ``job_events``, so without this the restored conversation would show
1036
- * the agent response with no visible prompt above it.
1043
+ * Replay one completed turn's already-fetched events, synchronously.
1044
+ *
1045
+ * Fetching is the caller's job (``StreamManager.restore`` loads every turn's
1046
+ * events in parallel and the message list once), so this is pure replay: no
1047
+ * awaits, so the whole restore runs in a single synchronous pass and the
1048
+ * consumer batches it into one render.
1049
+ *
1050
+ * ``userMessageContent`` is the prompt that triggered this turn. It's emitted
1051
+ * as a synthetic ``user_message`` BEFORE any of the turn's events: user
1052
+ * prompts aren't persisted in ``job_events``, and some events precede
1053
+ * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1054
+ * so leading with the prompt keeps the turn in order.
1055
+ */
1056
+ replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string): void;
1057
+ /**
1058
+ * Load a conversation's messages and replay its persisted history.
1059
+ *
1060
+ * Convenience for plain-``ChatSession`` consumers (the documented
1061
+ * conversation-management API). ``StreamManager`` drives restore itself —
1062
+ * loading messages once and replaying each turn in parallel — and does NOT
1063
+ * call this; it's kept so direct-Session usage doesn't break.
1064
+ *
1065
+ * Without ``jobId`` it replays the whole conversation; with one, just that
1066
+ * job's events.
1037
1067
  */
1038
- userMessageContent?: string): Promise<void>;
1068
+ switchConversation(id: string, jobId?: string): Promise<void>;
1039
1069
  deleteConversation(id: string): Promise<void>;
1040
1070
  toggleClientTool(name: string): boolean;
1041
1071
  }
@@ -1108,7 +1138,6 @@ declare function streamJobSSE(options: StreamJobSSEOptions): AsyncGenerator<Chat
1108
1138
 
1109
1139
  type StreamState = "idle" | "streaming" | "restoring" | "detached";
1110
1140
  interface SendOptions extends ModelChoiceOptions {
1111
- enableSearch?: boolean;
1112
1141
  agentName?: string;
1113
1142
  uploadIds?: string[];
1114
1143
  planMode?: boolean;
@@ -1151,7 +1180,26 @@ declare class StreamManager {
1151
1180
  private onSessionEvent;
1152
1181
  send(content: string, options?: SendOptions): Promise<void>;
1153
1182
  regenerate(): Promise<void>;
1154
- switchTo(conversationId: string): Promise<void>;
1183
+ /**
1184
+ * Switch the active conversation.
1185
+ *
1186
+ * ``opts.skipHistoryReplay`` is a fast path for consumers that CACHE a
1187
+ * restored conversation's rendered blocks: it moves the active pointer and
1188
+ * loads the message list (needed for send / regenerate context) but skips
1189
+ * the expensive event fetch + replay, and never enters the ``restoring``
1190
+ * state, so a consumer that clears its block view on ``restoring`` keeps
1191
+ * showing the cached history with no flash of a spinner.
1192
+ *
1193
+ * It still confirms there is no live job before skipping: the in-memory
1194
+ * background-job map is empty on a fresh instance (page reload) and blind to
1195
+ * jobs started in another tab/device, so the fast path always asks the server
1196
+ * (``getActiveJob``) and falls through to a full reconnect if one is running.
1197
+ * That one small request is the only cost it doesn't skip, so passing the
1198
+ * flag whenever you hold cached blocks is safe.
1199
+ */
1200
+ switchTo(conversationId: string, opts?: {
1201
+ skipHistoryReplay?: boolean;
1202
+ }): Promise<void>;
1155
1203
  createConversation(): Promise<string>;
1156
1204
  deleteConversation(id: string): Promise<void>;
1157
1205
  stop(): void;
@@ -1198,7 +1246,9 @@ declare function mapSseToChat(raw: RawSseEvent): ChatEvent[];
1198
1246
  * ``message_start`` (e.g. ``memory_recall``, emitted during prompt prep), so
1199
1247
  * gating the user block on ``message_start`` would replay them above the
1200
1248
  * user's own message. Keying off ``job_id`` injects the prompt once per job,
1201
- * before its first event — matching ``session.ts#switchConversation``.
1249
+ * before its first event — matching how the restore path
1250
+ * (``stream-manager.ts#restore`` → ``session.ts#replayTurn``) leads each turn
1251
+ * with its own prompt.
1202
1252
  */
1203
1253
  declare function replayEvents(sseEvents: RawSseEvent[], userMessages: {
1204
1254
  role: string;
package/dist/index.js CHANGED
@@ -510,7 +510,12 @@ var AstralformClient = class {
510
510
  thinking: m.thinking,
511
511
  tools: m.tools,
512
512
  vision: m.vision,
513
- thinkingMode: m.thinking_mode
513
+ thinkingMode: m.thinking_mode,
514
+ // Coerce so the non-optional `supportsEffort: boolean` stays honest even
515
+ // against an older backend that omits `supports_effort` (→ false = safe:
516
+ // the effort control is hidden). Unlike the always-present siblings above,
517
+ // this field can be absent, so it's the one that needs coercion.
518
+ supportsEffort: Boolean(m.supports_effort)
514
519
  }));
515
520
  }
516
521
  async getSkills() {
@@ -587,6 +592,10 @@ var AstralformClient = class {
587
592
  workspacePath: raw.workspace_path,
588
593
  sourceMessageId: raw.source_message_id,
589
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,
590
599
  createdAt: raw.created_at
591
600
  };
592
601
  }
@@ -1292,7 +1301,6 @@ var ChatSession = class {
1292
1301
  ),
1293
1302
  upload_ids: options?.uploadIds,
1294
1303
  agent_name: options?.agentName,
1295
- enable_search: options?.enableSearch,
1296
1304
  plan_mode: options?.planMode,
1297
1305
  // Per-request model choice (client-side model selection).
1298
1306
  provider: options?.provider,
@@ -1302,15 +1310,14 @@ var ChatSession = class {
1302
1310
  };
1303
1311
  await this.processStream(request);
1304
1312
  }
1305
- async resendFromCheckpoint(messageId, newContent, options) {
1313
+ async resendFromCheckpoint(messageId, newContent) {
1306
1314
  if (this.isStreaming) return;
1307
1315
  const request = {
1308
1316
  message: newContent,
1309
1317
  conversation_id: this.conversationId ?? void 0,
1310
1318
  resend_from: messageId,
1311
1319
  mcp_manifest: this.toolRegistry.getManifest(),
1312
- enabled_mcp: Array.from(this.enabledClientTools),
1313
- enable_search: options?.enableSearch
1320
+ enabled_mcp: Array.from(this.enabledClientTools)
1314
1321
  };
1315
1322
  await this.processStream(request);
1316
1323
  }
@@ -1501,6 +1508,20 @@ var ChatSession = class {
1501
1508
  }
1502
1509
  }
1503
1510
  }
1511
+ /**
1512
+ * Synchronous replay of a single stored wire event — the side-effect +
1513
+ * translate + emit core of ``dispatchWireEvent`` without the (live-only)
1514
+ * client-tool round-trip. Called in a tight synchronous loop during history
1515
+ * restore so the consumer's per-event store writes batch into ONE render
1516
+ * instead of re-typing the whole conversation event by event.
1517
+ */
1518
+ replayWireEvent(wire, conversationId) {
1519
+ this.applyWireSideEffects(wire, conversationId, "");
1520
+ const event = translateWireEvent(wire);
1521
+ if (event) {
1522
+ this.emit(event);
1523
+ }
1524
+ }
1504
1525
  /**
1505
1526
  * State mutations driven by wire events. Kept separate from translation so
1506
1527
  * the pure wire → ChatEvent mapping can live in translate.ts and be reused
@@ -1647,34 +1668,57 @@ var ChatSession = class {
1647
1668
  this.messages = [];
1648
1669
  return id;
1649
1670
  }
1650
- async switchConversation(id, jobId, userMessageContent) {
1671
+ /**
1672
+ * Replay one completed turn's already-fetched events, synchronously.
1673
+ *
1674
+ * Fetching is the caller's job (``StreamManager.restore`` loads every turn's
1675
+ * events in parallel and the message list once), so this is pure replay: no
1676
+ * awaits, so the whole restore runs in a single synchronous pass and the
1677
+ * consumer batches it into one render.
1678
+ *
1679
+ * ``userMessageContent`` is the prompt that triggered this turn. It's emitted
1680
+ * as a synthetic ``user_message`` BEFORE any of the turn's events: user
1681
+ * prompts aren't persisted in ``job_events``, and some events precede
1682
+ * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1683
+ * so leading with the prompt keeps the turn in order.
1684
+ */
1685
+ replayTurn(id, events, userMessageContent) {
1651
1686
  this.conversationId = id;
1652
1687
  this.resetStreamingState();
1688
+ if (userMessageContent) {
1689
+ this.emit({ type: "user_message", content: userMessageContent });
1690
+ }
1691
+ for (const ev of events) {
1692
+ const type = ev.data.type || ev.event;
1693
+ if (!type || type === "done") continue;
1694
+ const wire = { ...ev.data, type };
1695
+ try {
1696
+ this.replayWireEvent(wire, id);
1697
+ } catch {
1698
+ }
1699
+ }
1700
+ }
1701
+ /**
1702
+ * Load a conversation's messages and replay its persisted history.
1703
+ *
1704
+ * Convenience for plain-``ChatSession`` consumers (the documented
1705
+ * conversation-management API). ``StreamManager`` drives restore itself —
1706
+ * loading messages once and replaying each turn in parallel — and does NOT
1707
+ * call this; it's kept so direct-Session usage doesn't break.
1708
+ *
1709
+ * Without ``jobId`` it replays the whole conversation; with one, just that
1710
+ * job's events.
1711
+ */
1712
+ async switchConversation(id, jobId) {
1653
1713
  const [messagesResult, eventsResult] = await Promise.allSettled([
1654
1714
  this.client.getMessages(id).catch(() => this.storage.fetchMessages(id)),
1655
1715
  this.client.getConversationEvents(id, jobId)
1656
1716
  ]);
1657
1717
  this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
1658
- if (eventsResult.status === "fulfilled") {
1659
- if (userMessageContent) {
1660
- this.emit({ type: "user_message", content: userMessageContent });
1661
- }
1662
- for (const ev of eventsResult.value) {
1663
- const type = ev.data.type || ev.event;
1664
- if (!type || type === "done") continue;
1665
- const wire = { ...ev.data, type };
1666
- try {
1667
- await this.dispatchWireEvent(
1668
- wire,
1669
- id,
1670
- "",
1671
- false
1672
- // don't execute client tools on replay
1673
- );
1674
- } catch {
1675
- }
1676
- }
1677
- }
1718
+ this.replayTurn(
1719
+ id,
1720
+ eventsResult.status === "fulfilled" ? eventsResult.value : []
1721
+ );
1678
1722
  }
1679
1723
  async deleteConversation(id) {
1680
1724
  try {
@@ -1818,7 +1862,6 @@ var StreamManager = class {
1818
1862
  this.setState("streaming");
1819
1863
  try {
1820
1864
  await this.session.send(content, {
1821
- enableSearch: options?.enableSearch,
1822
1865
  agentName: options?.agentName,
1823
1866
  uploadIds: options?.uploadIds,
1824
1867
  planMode: options?.planMode,
@@ -1850,8 +1893,26 @@ var StreamManager = class {
1850
1893
  this.finalizeStream();
1851
1894
  }
1852
1895
  // ── Switch conversation ───────────────────────────────────────
1853
- async switchTo(conversationId) {
1896
+ /**
1897
+ * Switch the active conversation.
1898
+ *
1899
+ * ``opts.skipHistoryReplay`` is a fast path for consumers that CACHE a
1900
+ * restored conversation's rendered blocks: it moves the active pointer and
1901
+ * loads the message list (needed for send / regenerate context) but skips
1902
+ * the expensive event fetch + replay, and never enters the ``restoring``
1903
+ * state, so a consumer that clears its block view on ``restoring`` keeps
1904
+ * showing the cached history with no flash of a spinner.
1905
+ *
1906
+ * It still confirms there is no live job before skipping: the in-memory
1907
+ * background-job map is empty on a fresh instance (page reload) and blind to
1908
+ * jobs started in another tab/device, so the fast path always asks the server
1909
+ * (``getActiveJob``) and falls through to a full reconnect if one is running.
1910
+ * That one small request is the only cost it doesn't skip, so passing the
1911
+ * flag whenever you hold cached blocks is safe.
1912
+ */
1913
+ async switchTo(conversationId, opts) {
1854
1914
  if (conversationId === this._activeConversationId) return;
1915
+ const targetHadBackgroundJob = this._backgroundJobs.has(conversationId);
1855
1916
  if (this._state === "streaming") {
1856
1917
  const oldConvId = this._activeConversationId;
1857
1918
  const jobId = this.session.currentJobId;
@@ -1872,6 +1933,18 @@ var StreamManager = class {
1872
1933
  });
1873
1934
  }
1874
1935
  this.setActiveConversation(conversationId);
1936
+ if (opts?.skipHistoryReplay && !targetHadBackgroundJob) {
1937
+ let activeJobId = null;
1938
+ try {
1939
+ activeJobId = (await this.session.client.getActiveJob(conversationId)).jobId;
1940
+ } catch {
1941
+ }
1942
+ if (!activeJobId) {
1943
+ await this.session.loadConversation(conversationId);
1944
+ this.setState("idle");
1945
+ return;
1946
+ }
1947
+ }
1875
1948
  await this.restore(conversationId);
1876
1949
  }
1877
1950
  // ── Create / delete conversation ──────────────────────────────
@@ -1936,13 +2009,16 @@ var StreamManager = class {
1936
2009
  const userMessages = this.session.messages.filter(
1937
2010
  (m) => m.role === "user"
1938
2011
  );
2012
+ const eventLists = await Promise.all(
2013
+ completedJobs.map(
2014
+ (job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
2015
+ )
2016
+ );
1939
2017
  for (let i = 0; i < completedJobs.length; i++) {
1940
- const job = completedJobs[i];
1941
- const userContent = userMessages[i]?.content;
1942
- await this.session.switchConversation(
2018
+ this.session.replayTurn(
1943
2019
  conversationId,
1944
- job.job_id,
1945
- userContent
2020
+ eventLists[i] ?? [],
2021
+ userMessages[i]?.content
1946
2022
  );
1947
2023
  }
1948
2024
  if (completedJobs.length > 0) {