@runtypelabs/persona 4.20.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CG-bLFmk.d.cts → types-B_JdVK9o.d.cts} +2 -0
- package/dist/animations/{types-CG-bLFmk.d.ts → types-B_JdVK9o.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui.d.cts +22 -0
- package/dist/approval-ui.d.ts +22 -0
- package/dist/artifacts-ui.d.cts +22 -0
- package/dist/artifacts-ui.d.ts +22 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +22 -0
- package/dist/context-mentions.d.ts +22 -0
- package/dist/event-stream-view.d.cts +22 -0
- package/dist/event-stream-view.d.ts +22 -0
- package/dist/forms-ui.d.cts +46 -4
- package/dist/forms-ui.d.ts +46 -4
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +63 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.global.js +48 -48
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +63 -63
- package/dist/index.js.map +1 -1
- package/dist/session-reconnect.d.cts +22 -0
- package/dist/session-reconnect.d.ts +22 -0
- package/dist/smart-dom-reader.d.cts +22 -0
- package/dist/smart-dom-reader.d.ts +22 -0
- package/dist/theme-editor-preview.cjs +65 -65
- package/dist/theme-editor-preview.d.cts +22 -0
- package/dist/theme-editor-preview.d.ts +22 -0
- package/dist/theme-editor-preview.js +43 -43
- package/dist/theme-editor.d.cts +22 -0
- package/dist/theme-editor.d.ts +22 -0
- package/dist/webmcp-runtime.d.cts +16 -0
- package/dist/webmcp-runtime.d.ts +16 -0
- package/package.json +1 -1
- package/src/client-visitor-history.test.ts +473 -3
- package/src/client.test.ts +141 -3
- package/src/client.ts +157 -39
- package/src/generated/runtype-openapi-contract.ts +2 -0
- package/src/session.ts +139 -103
- package/src/session.webmcp.test.ts +48 -1
- package/src/types.ts +30 -0
- package/src/ui.history-lifecycle.test.ts +2 -2
- package/src/ui.ts +95 -7
|
@@ -35,6 +35,7 @@ const initBody = (overrides: Partial<ClientInitResponse> = {}): ClientInitRespon
|
|
|
35
35
|
conversationId: 'conv_1',
|
|
36
36
|
targetId: 'flow_1',
|
|
37
37
|
conversationRevision: 'rev_1',
|
|
38
|
+
durableRecovery: { enabled: true },
|
|
38
39
|
config: { welcomeMessage: null, placeholder: 'Ask...', theme: null },
|
|
39
40
|
...overrides,
|
|
40
41
|
});
|
|
@@ -85,6 +86,8 @@ type Harness = {
|
|
|
85
86
|
sessionInits: ClientSession[];
|
|
86
87
|
continuity: Array<{ previousConversationId: string | null; conversationId: string }>;
|
|
87
88
|
availability: boolean[];
|
|
89
|
+
durableResumePending: boolean;
|
|
90
|
+
resumableWrites: Array<{ executionId: string; after: string } | null>;
|
|
88
91
|
writes: string[];
|
|
89
92
|
};
|
|
90
93
|
|
|
@@ -92,6 +95,7 @@ const makeClient = (options: {
|
|
|
92
95
|
history?: boolean;
|
|
93
96
|
storedSessionId?: string | null;
|
|
94
97
|
storedConversationId?: string | null;
|
|
98
|
+
durableResumePending?: boolean;
|
|
95
99
|
historyBootstrapReady?: Promise<void>;
|
|
96
100
|
config?: Partial<AgentWidgetConfig>;
|
|
97
101
|
}): Harness => {
|
|
@@ -104,6 +108,8 @@ const makeClient = (options: {
|
|
|
104
108
|
sessionInits: [],
|
|
105
109
|
continuity: [],
|
|
106
110
|
availability: [],
|
|
111
|
+
durableResumePending: options.durableResumePending ?? false,
|
|
112
|
+
resumableWrites: [],
|
|
107
113
|
writes: [],
|
|
108
114
|
};
|
|
109
115
|
const internals: WidgetHistoryInternals = {
|
|
@@ -120,6 +126,11 @@ const makeClient = (options: {
|
|
|
120
126
|
harness.continuity!.push(info);
|
|
121
127
|
harness.writes!.push(`continuity:${info.previousConversationId}->${info.conversationId}`);
|
|
122
128
|
},
|
|
129
|
+
shouldResumeDurableConversation: () => harness.durableResumePending === true,
|
|
130
|
+
setStoredResumableHandle: (handle) => {
|
|
131
|
+
harness.durableResumePending = handle !== null;
|
|
132
|
+
harness.resumableWrites!.push(handle);
|
|
133
|
+
},
|
|
123
134
|
};
|
|
124
135
|
const config: AgentWidgetConfig = {
|
|
125
136
|
apiUrl: API_URL,
|
|
@@ -168,6 +179,7 @@ describe('client visitor history - init capability shape', () => {
|
|
|
168
179
|
body: {
|
|
169
180
|
token: CLIENT_TOKEN,
|
|
170
181
|
visitorHistory: true,
|
|
182
|
+
durableRecovery: true,
|
|
171
183
|
visitorToken: 'cvt_stored',
|
|
172
184
|
sessionId: 'sess_old',
|
|
173
185
|
},
|
|
@@ -175,7 +187,7 @@ describe('client visitor history - init capability shape', () => {
|
|
|
175
187
|
expect(requests[0].body).not.toHaveProperty('conversationId');
|
|
176
188
|
});
|
|
177
189
|
|
|
178
|
-
it('
|
|
190
|
+
it('negotiates recovery without enabling the history UI', async () => {
|
|
179
191
|
await seedToken('cvt_stored');
|
|
180
192
|
installFetch([ok()]);
|
|
181
193
|
const h = makeClient({ history: false, storedSessionId: 'sess_old' });
|
|
@@ -184,8 +196,11 @@ describe('client visitor history - init capability shape', () => {
|
|
|
184
196
|
|
|
185
197
|
expect(requests[0].body).toEqual({
|
|
186
198
|
token: CLIENT_TOKEN,
|
|
199
|
+
durableRecovery: true,
|
|
200
|
+
visitorToken: 'cvt_stored',
|
|
187
201
|
sessionId: 'sess_old',
|
|
188
202
|
});
|
|
203
|
+
expect(requests[0].body).not.toHaveProperty('conversationId');
|
|
189
204
|
});
|
|
190
205
|
|
|
191
206
|
it('normalizes targetId, preferring the top-level field over flow.id', async () => {
|
|
@@ -203,6 +218,41 @@ describe('client visitor history - init capability shape', () => {
|
|
|
203
218
|
expect(a.flow.id).toBe('flow_1');
|
|
204
219
|
expect(b.targetId).toBe('flow_7');
|
|
205
220
|
});
|
|
221
|
+
|
|
222
|
+
it('uses the exact visitor credential and durable cursor for reconnect', async () => {
|
|
223
|
+
await seedToken('cvt_stored');
|
|
224
|
+
const events = new Response('event: ping\ndata: {}\n\n', {
|
|
225
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
226
|
+
});
|
|
227
|
+
const fetchMock = vi
|
|
228
|
+
.fn()
|
|
229
|
+
.mockResolvedValueOnce(ok()())
|
|
230
|
+
.mockResolvedValueOnce(events);
|
|
231
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
232
|
+
const h = makeClient({});
|
|
233
|
+
await h.client.initSession();
|
|
234
|
+
|
|
235
|
+
const controller = new AbortController();
|
|
236
|
+
const response = await h.client.reconnectClientTokenStream({
|
|
237
|
+
executionId: 'exec_7',
|
|
238
|
+
after: '12',
|
|
239
|
+
signal: controller.signal,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(response).toBe(events);
|
|
243
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
244
|
+
`${API_URL}/v1/client/conversations/conv_1/executions/exec_7/events` +
|
|
245
|
+
'?sessionId=sess_1&after=12'
|
|
246
|
+
);
|
|
247
|
+
expect(fetchMock.mock.calls[1]?.[1]).toMatchObject({
|
|
248
|
+
method: 'GET',
|
|
249
|
+
headers: {
|
|
250
|
+
'X-Persona-Version': expect.any(String),
|
|
251
|
+
'X-Visitor-Token': 'cvt_stored',
|
|
252
|
+
},
|
|
253
|
+
signal: controller.signal,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
206
256
|
});
|
|
207
257
|
|
|
208
258
|
describe('client visitor history - mint persistence and immediate claim', () => {
|
|
@@ -313,6 +363,7 @@ describe('client visitor history - boot resume', () => {
|
|
|
313
363
|
expect(requests[0].body).toEqual({
|
|
314
364
|
token: CLIENT_TOKEN,
|
|
315
365
|
visitorHistory: true,
|
|
366
|
+
durableRecovery: true,
|
|
316
367
|
visitorToken: 'cvt_stored',
|
|
317
368
|
conversationId: 'conv_1',
|
|
318
369
|
});
|
|
@@ -331,6 +382,103 @@ describe('client visitor history - boot resume', () => {
|
|
|
331
382
|
expect(requests[0].body).not.toHaveProperty('conversationId');
|
|
332
383
|
});
|
|
333
384
|
|
|
385
|
+
it('reopens a durable conversation without enabling the history UI', async () => {
|
|
386
|
+
await seedToken('cvt_stored');
|
|
387
|
+
installFetch([ok({ sessionId: 'sess_resumed' })]);
|
|
388
|
+
const h = makeClient({
|
|
389
|
+
history: false,
|
|
390
|
+
storedSessionId: 'sess_old',
|
|
391
|
+
storedConversationId: 'conv_1',
|
|
392
|
+
durableResumePending: true,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await h.client.initSession();
|
|
396
|
+
|
|
397
|
+
expect(requests[0].body).toEqual({
|
|
398
|
+
token: CLIENT_TOKEN,
|
|
399
|
+
durableRecovery: true,
|
|
400
|
+
visitorToken: 'cvt_stored',
|
|
401
|
+
conversationId: 'conv_1',
|
|
402
|
+
});
|
|
403
|
+
expect(h.resumableWrites).toHaveLength(0);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('preserves the stored session without history or a durable handle', async () => {
|
|
407
|
+
await seedToken('cvt_stored');
|
|
408
|
+
installFetch([ok({ sessionId: 'sess_ordinary', conversationId: 'conv_2' })]);
|
|
409
|
+
const h = makeClient({
|
|
410
|
+
history: false,
|
|
411
|
+
storedSessionId: 'sess_old',
|
|
412
|
+
storedConversationId: 'conv_1',
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
await h.client.initSession();
|
|
416
|
+
|
|
417
|
+
expect(requests[0].body).toEqual({
|
|
418
|
+
token: CLIENT_TOKEN,
|
|
419
|
+
durableRecovery: true,
|
|
420
|
+
visitorToken: 'cvt_stored',
|
|
421
|
+
sessionId: 'sess_old',
|
|
422
|
+
});
|
|
423
|
+
expect(requests[0].body).not.toHaveProperty('conversationId');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('drops a pending handle when the server does not enable recovery', async () => {
|
|
427
|
+
await seedToken('cvt_stored');
|
|
428
|
+
installFetch([
|
|
429
|
+
ok({ sessionId: 'sess_resumed', durableRecovery: { enabled: false } }),
|
|
430
|
+
]);
|
|
431
|
+
const h = makeClient({
|
|
432
|
+
history: false,
|
|
433
|
+
storedConversationId: 'conv_1',
|
|
434
|
+
durableResumePending: true,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const session = await h.client.initSession();
|
|
438
|
+
|
|
439
|
+
expect(requests).toHaveLength(1);
|
|
440
|
+
expect(requests[0].body).toMatchObject({ conversationId: 'conv_1' });
|
|
441
|
+
expect(session.durableRecovery).toEqual({ enabled: false });
|
|
442
|
+
expect(h.resumableWrites).toEqual([null]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('drops a pending handle when an old server omits the recovery capability', async () => {
|
|
446
|
+
await seedToken('cvt_stored');
|
|
447
|
+
installFetch([ok({ sessionId: 'sess_legacy', durableRecovery: undefined })]);
|
|
448
|
+
const h = makeClient({
|
|
449
|
+
history: false,
|
|
450
|
+
storedConversationId: 'conv_1',
|
|
451
|
+
durableResumePending: true,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const session = await h.client.initSession();
|
|
455
|
+
|
|
456
|
+
expect(session.durableRecovery).toBeUndefined();
|
|
457
|
+
expect(h.resumableWrites).toEqual([null]);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('clears a stale pending handle before its single ordinary fallback', async () => {
|
|
461
|
+
await seedToken('cvt_stored');
|
|
462
|
+
installFetch([
|
|
463
|
+
fail(404, { error: 'Conversation not found' }),
|
|
464
|
+
ok({ sessionId: 'sess_new', conversationId: 'conv_2' }),
|
|
465
|
+
]);
|
|
466
|
+
const h = makeClient({
|
|
467
|
+
history: false,
|
|
468
|
+
storedSessionId: 'sess_old',
|
|
469
|
+
storedConversationId: 'conv_1',
|
|
470
|
+
durableResumePending: true,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
await h.client.initSession();
|
|
474
|
+
|
|
475
|
+
expect(requests).toHaveLength(2);
|
|
476
|
+
expect(requests[0].body).toMatchObject({ conversationId: 'conv_1' });
|
|
477
|
+
expect(requests[1].body).toMatchObject({ sessionId: 'sess_old' });
|
|
478
|
+
expect(requests[1].body).not.toHaveProperty('conversationId');
|
|
479
|
+
expect(h.resumableWrites).toEqual([null]);
|
|
480
|
+
});
|
|
481
|
+
|
|
334
482
|
it('falls back exactly once on a resume 404 and reports the continuity break', async () => {
|
|
335
483
|
await seedToken('cvt_stored');
|
|
336
484
|
installFetch([
|
|
@@ -417,6 +565,7 @@ describe('client visitor history - prepared sessions', () => {
|
|
|
417
565
|
expect(requests[1].body).toEqual({
|
|
418
566
|
token: CLIENT_TOKEN,
|
|
419
567
|
visitorHistory: true,
|
|
568
|
+
durableRecovery: true,
|
|
420
569
|
visitorToken: 'cvt_stored',
|
|
421
570
|
conversationId: 'conv_other',
|
|
422
571
|
});
|
|
@@ -478,6 +627,7 @@ describe('client visitor history - prepared sessions', () => {
|
|
|
478
627
|
expect(requests[0].body).toEqual({
|
|
479
628
|
token: CLIENT_TOKEN,
|
|
480
629
|
visitorHistory: true,
|
|
630
|
+
durableRecovery: true,
|
|
481
631
|
visitorToken: 'cvt_stored',
|
|
482
632
|
});
|
|
483
633
|
expect(prepared.session.sessionId).toBe('sess_new');
|
|
@@ -504,10 +654,16 @@ describe('client visitor history - 403 degrade', () => {
|
|
|
504
654
|
expect(h.availability).toEqual([false]);
|
|
505
655
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
506
656
|
|
|
507
|
-
//
|
|
657
|
+
// History remains latched off, while recovery negotiates independently
|
|
658
|
+
// without breaking ordinary session continuity.
|
|
508
659
|
h.client.clearClientSession();
|
|
509
660
|
await h.client.initSession();
|
|
510
|
-
expect(requests[2].body).toEqual({
|
|
661
|
+
expect(requests[2].body).toEqual({
|
|
662
|
+
token: CLIENT_TOKEN,
|
|
663
|
+
durableRecovery: true,
|
|
664
|
+
visitorToken: 'cvt_stored',
|
|
665
|
+
sessionId: 'sess_plain',
|
|
666
|
+
});
|
|
511
667
|
});
|
|
512
668
|
|
|
513
669
|
it('rejects rather than degrading a conversation resume into another record', async () => {
|
|
@@ -566,6 +722,7 @@ describe('controller wiring', () => {
|
|
|
566
722
|
expect(requests[0].body).toEqual({
|
|
567
723
|
token: CLIENT_TOKEN,
|
|
568
724
|
visitorHistory: true,
|
|
725
|
+
durableRecovery: true,
|
|
569
726
|
visitorToken: 'cvt_stored',
|
|
570
727
|
conversationId: 'conv_stored',
|
|
571
728
|
});
|
|
@@ -573,6 +730,319 @@ describe('controller wiring', () => {
|
|
|
573
730
|
controller.destroy();
|
|
574
731
|
mount.remove();
|
|
575
732
|
});
|
|
733
|
+
|
|
734
|
+
it('persists the built-in durable handle without history UI config and clears it at terminal', async () => {
|
|
735
|
+
await seedToken('cvt_stored');
|
|
736
|
+
installFetch([ok({ sessionId: 'sess_ui' })]);
|
|
737
|
+
const mount = document.createElement('div');
|
|
738
|
+
document.body.appendChild(mount);
|
|
739
|
+
const controller = createAgentExperience(mount, {
|
|
740
|
+
apiUrl: API_URL,
|
|
741
|
+
clientToken: CLIENT_TOKEN,
|
|
742
|
+
launcher: { enabled: false },
|
|
743
|
+
});
|
|
744
|
+
await vi.waitFor(() => expect(requests).toHaveLength(1));
|
|
745
|
+
|
|
746
|
+
let streamController!: ReadableStreamDefaultController<Uint8Array>;
|
|
747
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
748
|
+
start(value) {
|
|
749
|
+
streamController = value;
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
const connected = controller.connectStream(stream);
|
|
753
|
+
const encoder = new TextEncoder();
|
|
754
|
+
streamController.enqueue(
|
|
755
|
+
encoder.encode(
|
|
756
|
+
'id: 7\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_ui","id":"text_ui","delta":"Hi"}\n\n'
|
|
757
|
+
)
|
|
758
|
+
);
|
|
759
|
+
await vi.waitFor(() =>
|
|
760
|
+
expect(controller.getPersistentMetadata().durableResume).toEqual({
|
|
761
|
+
executionId: 'exec_ui',
|
|
762
|
+
after: '7',
|
|
763
|
+
})
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
streamController.enqueue(
|
|
767
|
+
encoder.encode(
|
|
768
|
+
'id: 8\nevent: execution_complete\ndata: {"type":"execution_complete","executionId":"exec_ui","kind":"agent","success":true}\n\n'
|
|
769
|
+
)
|
|
770
|
+
);
|
|
771
|
+
streamController.close();
|
|
772
|
+
await connected;
|
|
773
|
+
expect(controller.getPersistentMetadata()).not.toHaveProperty('durableResume');
|
|
774
|
+
|
|
775
|
+
controller.destroy();
|
|
776
|
+
mount.remove();
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it('retains the built-in durable handle during page exit', async () => {
|
|
780
|
+
await seedToken('cvt_stored');
|
|
781
|
+
installFetch([ok({ sessionId: 'sess_ui' })]);
|
|
782
|
+
const mount = document.createElement('div');
|
|
783
|
+
document.body.appendChild(mount);
|
|
784
|
+
const controller = createAgentExperience(mount, {
|
|
785
|
+
apiUrl: API_URL,
|
|
786
|
+
clientToken: CLIENT_TOKEN,
|
|
787
|
+
launcher: { enabled: false },
|
|
788
|
+
});
|
|
789
|
+
await vi.waitFor(() => expect(requests).toHaveLength(1));
|
|
790
|
+
|
|
791
|
+
let firstStreamController!: ReadableStreamDefaultController<Uint8Array>;
|
|
792
|
+
const firstStream = new ReadableStream<Uint8Array>({
|
|
793
|
+
start(value) {
|
|
794
|
+
firstStreamController = value;
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
const firstConnected = controller.connectStream(firstStream);
|
|
798
|
+
const encoder = new TextEncoder();
|
|
799
|
+
firstStreamController.enqueue(
|
|
800
|
+
encoder.encode(
|
|
801
|
+
'id: 7\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_exit","id":"text_exit","delta":"Hi"}\n\n'
|
|
802
|
+
)
|
|
803
|
+
);
|
|
804
|
+
await vi.waitFor(() =>
|
|
805
|
+
expect(controller.getPersistentMetadata().durableResume).toEqual({
|
|
806
|
+
executionId: 'exec_exit',
|
|
807
|
+
after: '7',
|
|
808
|
+
})
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
window.dispatchEvent(new Event('pagehide'));
|
|
812
|
+
firstStreamController.enqueue(
|
|
813
|
+
encoder.encode(
|
|
814
|
+
'id: 8\nevent: execution_complete\ndata: {"type":"execution_complete","executionId":"exec_exit","kind":"agent","success":true}\n\n'
|
|
815
|
+
)
|
|
816
|
+
);
|
|
817
|
+
firstStreamController.close();
|
|
818
|
+
await firstConnected;
|
|
819
|
+
expect(controller.getPersistentMetadata().durableResume).toEqual({
|
|
820
|
+
executionId: 'exec_exit',
|
|
821
|
+
after: '7',
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
controller.destroy();
|
|
825
|
+
mount.remove();
|
|
826
|
+
});
|
|
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
|
+
|
|
959
|
+
it('uses Persona-owned stored recovery state to reopen the durable conversation', async () => {
|
|
960
|
+
await seedToken('cvt_stored');
|
|
961
|
+
global.fetch = vi.fn(async (url: string | URL | Request, options?: RequestInit) => {
|
|
962
|
+
if (String(url) === INIT_URL) {
|
|
963
|
+
requests.push({
|
|
964
|
+
url: String(url),
|
|
965
|
+
body: JSON.parse(String(options?.body)) as Record<string, unknown>,
|
|
966
|
+
headers: (options?.headers ?? {}) as Record<string, string>,
|
|
967
|
+
});
|
|
968
|
+
return new Response(
|
|
969
|
+
JSON.stringify(
|
|
970
|
+
initBody({
|
|
971
|
+
sessionId: 'sess_resumed',
|
|
972
|
+
conversationId: 'conv_durable',
|
|
973
|
+
})
|
|
974
|
+
),
|
|
975
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
return new Response(
|
|
979
|
+
'id: 20\nevent: execution_complete\ndata: {"type":"execution_complete","executionId":"exec_durable","kind":"agent","success":true}\n\n',
|
|
980
|
+
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } }
|
|
981
|
+
);
|
|
982
|
+
}) as unknown as typeof fetch;
|
|
983
|
+
const mount = document.createElement('div');
|
|
984
|
+
document.body.appendChild(mount);
|
|
985
|
+
const controller = createAgentExperience(mount, {
|
|
986
|
+
apiUrl: API_URL,
|
|
987
|
+
clientToken: CLIENT_TOKEN,
|
|
988
|
+
launcher: { enabled: false },
|
|
989
|
+
storageAdapter: {
|
|
990
|
+
load: async () => ({
|
|
991
|
+
messages: [],
|
|
992
|
+
metadata: {
|
|
993
|
+
conversationId: 'conv_durable',
|
|
994
|
+
durableResume: { executionId: 'exec_durable', after: '19' },
|
|
995
|
+
},
|
|
996
|
+
}),
|
|
997
|
+
save: () => undefined,
|
|
998
|
+
},
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
await vi.waitFor(() => expect(requests).toHaveLength(1));
|
|
1002
|
+
expect(requests[0].body).toMatchObject({
|
|
1003
|
+
durableRecovery: true,
|
|
1004
|
+
visitorToken: 'cvt_stored',
|
|
1005
|
+
conversationId: 'conv_durable',
|
|
1006
|
+
});
|
|
1007
|
+
expect(requests[0].body).not.toHaveProperty('visitorHistory');
|
|
1008
|
+
|
|
1009
|
+
controller.destroy();
|
|
1010
|
+
mount.remove();
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('does not pass Persona-stored recovery state to a custom reconnect transport', async () => {
|
|
1014
|
+
await seedToken('cvt_stored');
|
|
1015
|
+
installFetch([ok({ sessionId: 'sess_ui' })]);
|
|
1016
|
+
const reconnectStream = vi.fn(
|
|
1017
|
+
() => new Promise<Response>(() => undefined)
|
|
1018
|
+
);
|
|
1019
|
+
const mount = document.createElement('div');
|
|
1020
|
+
document.body.appendChild(mount);
|
|
1021
|
+
const controller = createAgentExperience(mount, {
|
|
1022
|
+
apiUrl: API_URL,
|
|
1023
|
+
clientToken: CLIENT_TOKEN,
|
|
1024
|
+
launcher: { enabled: false },
|
|
1025
|
+
reconnectStream,
|
|
1026
|
+
storageAdapter: {
|
|
1027
|
+
load: async () => ({
|
|
1028
|
+
messages: [],
|
|
1029
|
+
metadata: {
|
|
1030
|
+
conversationId: 'conv_host',
|
|
1031
|
+
durableResume: { executionId: 'exec_host', after: '19' },
|
|
1032
|
+
},
|
|
1033
|
+
}),
|
|
1034
|
+
save: () => undefined,
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
await vi.waitFor(() => expect(requests).toHaveLength(1));
|
|
1039
|
+
expect(requests[0].body).not.toHaveProperty('conversationId');
|
|
1040
|
+
expect(controller.getStatus()).toBe('idle');
|
|
1041
|
+
expect(reconnectStream).not.toHaveBeenCalled();
|
|
1042
|
+
|
|
1043
|
+
controller.destroy();
|
|
1044
|
+
mount.remove();
|
|
1045
|
+
});
|
|
576
1046
|
});
|
|
577
1047
|
|
|
578
1048
|
describe('client visitor history - dedupe', () => {
|