@runtypelabs/persona 4.21.0 → 4.21.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "4.21.0",
3
+ "version": "4.21.1",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -825,6 +825,137 @@ describe('controller wiring', () => {
825
825
  mount.remove();
826
826
  });
827
827
 
828
+ it('persists the latest durable cursor with the transcript before page exit', async () => {
829
+ await seedToken('cvt_stored');
830
+ installFetch([ok({ sessionId: 'sess_ui' })]);
831
+ const mount = document.createElement('div');
832
+ document.body.appendChild(mount);
833
+ const controller = createAgentExperience(mount, {
834
+ apiUrl: API_URL,
835
+ clientToken: CLIENT_TOKEN,
836
+ launcher: { enabled: false },
837
+ });
838
+ await vi.waitFor(() => expect(requests).toHaveLength(1));
839
+
840
+ let streamController!: ReadableStreamDefaultController<Uint8Array>;
841
+ const stream = new ReadableStream<Uint8Array>({
842
+ start(value) {
843
+ streamController = value;
844
+ },
845
+ });
846
+ const connected = controller.connectStream(stream);
847
+ const encoder = new TextEncoder();
848
+ streamController.enqueue(
849
+ encoder.encode(
850
+ 'id: 7\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_exit_latest","id":"text_exit_latest","delta":"Hi"}\n\n'
851
+ )
852
+ );
853
+ await vi.waitFor(() =>
854
+ expect(controller.getPersistentMetadata().durableResume).toEqual({
855
+ executionId: 'exec_exit_latest',
856
+ after: '7',
857
+ })
858
+ );
859
+
860
+ streamController.enqueue(
861
+ encoder.encode(
862
+ 'id: 8\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_exit_latest","id":"text_exit_latest","delta":" there"}\n\n'
863
+ )
864
+ );
865
+ await vi.waitFor(() =>
866
+ expect(controller.getMessages().find((message) => message.role === 'assistant')?.content).toBe(
867
+ 'Hi there'
868
+ )
869
+ );
870
+ await vi.waitFor(() =>
871
+ expect(controller.getPersistentMetadata().durableResume).toEqual({
872
+ executionId: 'exec_exit_latest',
873
+ after: '8',
874
+ })
875
+ );
876
+
877
+ window.dispatchEvent(new Event('pagehide'));
878
+
879
+ streamController.close();
880
+ await connected;
881
+ controller.destroy();
882
+ mount.remove();
883
+ });
884
+
885
+ it('keeps async transcript snapshots aligned with the durable cursor', async () => {
886
+ await seedToken('cvt_stored');
887
+ installFetch([ok({ sessionId: 'sess_ui' })]);
888
+ const saves: AgentWidgetStoredState[] = [];
889
+ const mount = document.createElement('div');
890
+ document.body.appendChild(mount);
891
+ const controller = createAgentExperience(mount, {
892
+ apiUrl: API_URL,
893
+ clientToken: CLIENT_TOKEN,
894
+ launcher: { enabled: false },
895
+ storageAdapter: {
896
+ save: async (state) => {
897
+ await Promise.resolve();
898
+ saves.push(state);
899
+ },
900
+ },
901
+ });
902
+ await vi.waitFor(() => expect(requests).toHaveLength(1));
903
+
904
+ let streamController!: ReadableStreamDefaultController<Uint8Array>;
905
+ const stream = new ReadableStream<Uint8Array>({
906
+ start(value) {
907
+ streamController = value;
908
+ },
909
+ });
910
+ const connected = controller.connectStream(stream);
911
+ const encoder = new TextEncoder();
912
+ streamController.enqueue(
913
+ encoder.encode(
914
+ 'id: 7\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_async","id":"text_async","delta":"Hi"}\n\n'
915
+ )
916
+ );
917
+ await vi.waitFor(() =>
918
+ expect(controller.getPersistentMetadata().durableResume).toEqual({
919
+ executionId: 'exec_async',
920
+ after: '7',
921
+ })
922
+ );
923
+
924
+ streamController.enqueue(
925
+ encoder.encode(
926
+ 'id: 8\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_async","id":"text_async","delta":" there"}\n\n'
927
+ )
928
+ );
929
+ let firstCompleteTranscript: AgentWidgetStoredState | undefined;
930
+ await vi.waitFor(() => {
931
+ firstCompleteTranscript = saves.find((state) =>
932
+ state.messages?.some(
933
+ (message) => message.role === 'assistant' && message.content === 'Hi there'
934
+ )
935
+ );
936
+ expect(firstCompleteTranscript).toBeDefined();
937
+ });
938
+ expect(firstCompleteTranscript?.metadata?.durableResume).toEqual({
939
+ executionId: 'exec_async',
940
+ after: '8',
941
+ });
942
+ await Promise.resolve();
943
+ const savesBeforeExit = saves.length;
944
+ window.dispatchEvent(new Event('pagehide'));
945
+ await Promise.resolve();
946
+ expect(saves).toHaveLength(savesBeforeExit);
947
+
948
+ streamController.enqueue(
949
+ encoder.encode(
950
+ 'id: 9\nevent: execution_complete\ndata: {"type":"execution_complete","executionId":"exec_async","kind":"agent","success":true}\n\n'
951
+ )
952
+ );
953
+ streamController.close();
954
+ await connected;
955
+ controller.destroy();
956
+ mount.remove();
957
+ });
958
+
828
959
  it('uses Persona-owned stored recovery state to reopen the durable conversation', async () => {
829
960
  await seedToken('cvt_stored');
830
961
  global.fetch = vi.fn(async (url: string | URL | Request, options?: RequestInit) => {
package/src/ui.ts CHANGED
@@ -1054,6 +1054,8 @@ export const createAgentExperience = (
1054
1054
  messagePersistenceDisabled
1055
1055
  ? null
1056
1056
  : (config.storageAdapter ?? createLocalStorageAdapter());
1057
+ const personaOwnsClientTokenRecovery =
1058
+ Boolean(config.clientToken) && typeof config.reconnectStream !== "function";
1057
1059
  let persistentMetadata: Record<string, unknown> = {};
1058
1060
  let pendingStoredState: Promise<AgentWidgetStoredState | null> | null = null;
1059
1061
  // Resolves once stored state has been applied (or its load failed). Every
@@ -5313,6 +5315,17 @@ export const createAgentExperience = (
5313
5315
  ? getMessagesForPersistence()
5314
5316
  : [];
5315
5317
 
5318
+ const durableHandle =
5319
+ personaOwnsClientTokenRecovery && session?.getResumableHandle();
5320
+ if (durableHandle) {
5321
+ persistentMetadata = {
5322
+ ...persistentMetadata,
5323
+ durableResume: {
5324
+ executionId: durableHandle.executionId,
5325
+ after: durableHandle.lastEventId,
5326
+ },
5327
+ };
5328
+ }
5316
5329
  const payload: AgentWidgetStoredState = {
5317
5330
  messages,
5318
5331
  metadata: persistentMetadata,
@@ -8384,8 +8397,6 @@ export const createAgentExperience = (
8384
8397
  ? { executionId: candidate.executionId, after: candidate.after }
8385
8398
  : null;
8386
8399
  };
8387
- const personaOwnsClientTokenRecovery =
8388
- Boolean(config.clientToken) && typeof config.reconnectStream !== "function";
8389
8400
  if (config.clientToken) {
8390
8401
  config = {
8391
8402
  ...config,
@@ -8498,17 +8509,11 @@ export const createAgentExperience = (
8498
8509
  const hostExecutionState = config.onExecutionState;
8499
8510
  const recoveryWindow = mount.ownerDocument.defaultView;
8500
8511
  let pageExitInProgress = false;
8501
- const markPageExit = () => {
8502
- pageExitInProgress = true;
8503
- };
8504
- const markPageActive = () => {
8505
- pageExitInProgress = false;
8506
- };
8507
- recoveryWindow?.addEventListener("beforeunload", markPageExit);
8512
+ const markPageExit = () => (pageExitInProgress = true);
8513
+ const markPageActive = () => (pageExitInProgress = false);
8508
8514
  recoveryWindow?.addEventListener("pagehide", markPageExit);
8509
8515
  recoveryWindow?.addEventListener("pageshow", markPageActive);
8510
8516
  destroyCallbacks.push(() => {
8511
- recoveryWindow?.removeEventListener("beforeunload", markPageExit);
8512
8517
  recoveryWindow?.removeEventListener("pagehide", markPageExit);
8513
8518
  recoveryWindow?.removeEventListener("pageshow", markPageActive);
8514
8519
  });
@@ -8638,6 +8643,18 @@ export const createAgentExperience = (
8638
8643
  let activeStreamingTextCandidate: { index: number; id: string } | null = null;
8639
8644
  let pendingStreamingTextMessages: AgentWidgetMessage[] | null = null;
8640
8645
  let streamingTextRAF: number | null = null;
8646
+ let pendingDurablePersistenceMessages: AgentWidgetMessage[] | null = null;
8647
+
8648
+ const persistMessages = (messages: AgentWidgetMessage[]) => {
8649
+ if (!personaOwnsClientTokenRecovery) return persistState(messages);
8650
+ pendingDurablePersistenceMessages = messages;
8651
+ queueMicrotask(() => {
8652
+ if (pendingDurablePersistenceMessages === messages) {
8653
+ pendingDurablePersistenceMessages = null;
8654
+ persistState(messages);
8655
+ }
8656
+ });
8657
+ };
8641
8658
 
8642
8659
  const applyMessagesChanged = (messages: AgentWidgetMessage[]) => {
8643
8660
  lastAppliedMessages = messages;
@@ -8698,7 +8715,7 @@ export const createAgentExperience = (
8698
8715
  }
8699
8716
 
8700
8717
  voiceState.lastUserMessageWasVoice = Boolean(lastUserMessage?.viaVoice);
8701
- persistState(messages);
8718
+ persistMessages(messages);
8702
8719
  syncComposerBarPeek();
8703
8720
  };
8704
8721