@volley/recognition-client-sdk 0.1.385 → 0.1.418

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.
@@ -5,7 +5,7 @@
5
5
  import { SimplifiedVGFRecognitionClient, createSimplifiedVGFClient } from './simplified-vgf-recognition-client.js';
6
6
  import { RealTimeTwoWayWebSocketRecognitionClient } from './recognition-client.js';
7
7
  import { ClientState } from './recognition-client.types.js';
8
- import { AudioEncoding, RecognitionContextTypeV1 } from '@recog/shared-types';
8
+ import { AudioEncoding, RecognitionContextTypeV1, RecognitionResultTypeV1 } from '@recog/shared-types';
9
9
  import {
10
10
  RecordingStatus,
11
11
  TranscriptionStatus,
@@ -287,21 +287,43 @@ describe('SimplifiedVGFRecognitionClient', () => {
287
287
  expect(updatedState.finalTranscriptionTimestamp).toBeDefined();
288
288
  });
289
289
 
290
- it('should handle metadata and mark recording as finished', () => {
290
+ it('should pass metadata to callback without updating VGF state', () => {
291
291
  // Get the actual UUID from the client
292
292
  const actualUuid = simplifiedClient.getVGFState().audioUtteranceId;
293
+ const originalOnMetadata = jest.fn();
293
294
 
295
+ // Create new client with onMetadata callback
296
+ const clientWithMetadata = new SimplifiedVGFRecognitionClient({
297
+ asrRequestConfig: {
298
+ provider: 'deepgram',
299
+ language: 'en',
300
+ sampleRate: 16000,
301
+ encoding: AudioEncoding.LINEAR16
302
+ },
303
+ onStateChange: stateChangeCallback,
304
+ onMetadata: originalOnMetadata
305
+ });
306
+
307
+ const constructorCalls = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls;
308
+ const latestConfig = constructorCalls[constructorCalls.length - 1]?.[0];
309
+ const metadataCallback = latestConfig?.onMetadata;
310
+
311
+ const clientUuid = clientWithMetadata.getVGFState().audioUtteranceId;
294
312
  const metadata = {
295
- audioUtteranceId: actualUuid,
296
- duration: 5000
313
+ type: RecognitionResultTypeV1.METADATA as const,
314
+ audioUtteranceId: clientUuid
297
315
  };
298
316
 
299
- onMetadataCallback(metadata);
317
+ // Clear previous state changes from client creation
318
+ stateChangeCallback.mockClear();
300
319
 
301
- expect(stateChangeCallback).toHaveBeenCalled();
302
- const updatedState = stateChangeCallback.mock.calls[0][0];
303
- expect(updatedState.startRecordingStatus).toBe(RecordingStatus.FINISHED);
304
- expect(updatedState.finalRecordingTimestamp).toBeDefined();
320
+ metadataCallback?.(metadata);
321
+
322
+ // Metadata should NOT trigger state change (state management simplified)
323
+ expect(stateChangeCallback).not.toHaveBeenCalled();
324
+
325
+ // But original callback should still be called
326
+ expect(originalOnMetadata).toHaveBeenCalledWith(metadata);
305
327
  });
306
328
 
307
329
  it('should handle errors and update state', () => {
@@ -325,13 +347,18 @@ describe('SimplifiedVGFRecognitionClient', () => {
325
347
  // Then error occurs
326
348
  onErrorCallback({ message: 'Error' });
327
349
 
328
- // Send audio again should restart recording
350
+ // After terminal status (ERROR), no more state changes should be emitted
351
+ // The session is considered over, so sendAudio should not trigger callbacks
329
352
  stateChangeCallback.mockClear();
330
353
  simplifiedClient.sendAudio(Buffer.from([4, 5, 6]));
331
354
 
332
- const updatedState = stateChangeCallback.mock.calls[0][0];
333
- expect(updatedState.startRecordingStatus).toBe(RecordingStatus.RECORDING);
334
- expect(updatedState.startRecordingTimestamp).toBeDefined();
355
+ // No callback should be triggered since we're in terminal state
356
+ expect(stateChangeCallback).not.toHaveBeenCalled();
357
+
358
+ // However, the internal isRecordingAudio flag should still be reset
359
+ // (verified by the fact that the flag was set during the first sendAudio)
360
+ const state = simplifiedClient.getVGFState();
361
+ expect(state.transcriptionStatus).toBe(TranscriptionStatus.ERROR);
335
362
  });
336
363
  });
337
364
 
@@ -1331,4 +1358,163 @@ describe('SimplifiedVGFRecognitionClient', () => {
1331
1358
  expect(stateChangeCallback).not.toHaveBeenCalled();
1332
1359
  });
1333
1360
  });
1361
+
1362
+ describe('Terminal Status Protection', () => {
1363
+ let stateChangeCallback: jest.Mock;
1364
+ let onTranscriptCallback: (result: any) => void;
1365
+ let onErrorCallback: (error: any) => void;
1366
+ let simplifiedClient: SimplifiedVGFRecognitionClient;
1367
+ let clientUuid: string;
1368
+
1369
+ beforeEach(() => {
1370
+ stateChangeCallback = jest.fn();
1371
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
1372
+ asrRequestConfig: {
1373
+ provider: 'deepgram',
1374
+ language: 'en',
1375
+ sampleRate: 16000,
1376
+ encoding: AudioEncoding.LINEAR16
1377
+ },
1378
+ onStateChange: stateChangeCallback
1379
+ });
1380
+
1381
+ const constructorCalls = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls;
1382
+ const latestConfig = constructorCalls[constructorCalls.length - 1]?.[0];
1383
+ onTranscriptCallback = latestConfig?.onTranscript ?? jest.fn();
1384
+ onErrorCallback = latestConfig?.onError ?? jest.fn();
1385
+ clientUuid = simplifiedClient.getVGFState().audioUtteranceId;
1386
+ });
1387
+
1388
+ it('should allow first terminal transcript callback', () => {
1389
+ onTranscriptCallback({
1390
+ type: 'Transcription',
1391
+ audioUtteranceId: clientUuid,
1392
+ finalTranscript: 'hello',
1393
+ finalTranscriptConfidence: 0.9,
1394
+ is_finished: true
1395
+ });
1396
+
1397
+ expect(stateChangeCallback).toHaveBeenCalledTimes(1);
1398
+ expect(stateChangeCallback.mock.calls[0][0].transcriptionStatus).toBe(TranscriptionStatus.FINALIZED);
1399
+ });
1400
+
1401
+ it('should block second terminal transcript callback with same UUID', () => {
1402
+ // First terminal
1403
+ onTranscriptCallback({
1404
+ type: 'Transcription',
1405
+ audioUtteranceId: clientUuid,
1406
+ finalTranscript: 'first',
1407
+ finalTranscriptConfidence: 0.9,
1408
+ is_finished: true
1409
+ });
1410
+
1411
+ stateChangeCallback.mockClear();
1412
+
1413
+ // Second terminal - should be blocked
1414
+ onTranscriptCallback({
1415
+ type: 'Transcription',
1416
+ audioUtteranceId: clientUuid,
1417
+ finalTranscript: 'second',
1418
+ finalTranscriptConfidence: 0.95,
1419
+ is_finished: true
1420
+ });
1421
+
1422
+ expect(stateChangeCallback).not.toHaveBeenCalled();
1423
+ });
1424
+
1425
+ it('should allow first error callback', () => {
1426
+ onErrorCallback({
1427
+ audioUtteranceId: clientUuid,
1428
+ message: 'Error occurred'
1429
+ });
1430
+
1431
+ expect(stateChangeCallback).toHaveBeenCalledTimes(1);
1432
+ expect(stateChangeCallback.mock.calls[0][0].transcriptionStatus).toBe(TranscriptionStatus.ERROR);
1433
+ });
1434
+
1435
+ it('should block second error callback with same UUID', () => {
1436
+ // First error
1437
+ onErrorCallback({
1438
+ audioUtteranceId: clientUuid,
1439
+ message: 'First error'
1440
+ });
1441
+
1442
+ stateChangeCallback.mockClear();
1443
+
1444
+ // Second error - should be blocked
1445
+ onErrorCallback({
1446
+ audioUtteranceId: clientUuid,
1447
+ message: 'Second error'
1448
+ });
1449
+
1450
+ expect(stateChangeCallback).not.toHaveBeenCalled();
1451
+ });
1452
+
1453
+ it('should block transcript after error', () => {
1454
+ // Error first
1455
+ onErrorCallback({
1456
+ audioUtteranceId: clientUuid,
1457
+ message: 'Error'
1458
+ });
1459
+
1460
+ stateChangeCallback.mockClear();
1461
+
1462
+ // Transcript after error - should be blocked
1463
+ onTranscriptCallback({
1464
+ type: 'Transcription',
1465
+ audioUtteranceId: clientUuid,
1466
+ finalTranscript: 'late',
1467
+ finalTranscriptConfidence: 0.9,
1468
+ is_finished: true
1469
+ });
1470
+
1471
+ expect(stateChangeCallback).not.toHaveBeenCalled();
1472
+ });
1473
+
1474
+ it('should block error after terminal transcript', () => {
1475
+ // Terminal transcript first
1476
+ onTranscriptCallback({
1477
+ type: 'Transcription',
1478
+ audioUtteranceId: clientUuid,
1479
+ finalTranscript: 'done',
1480
+ finalTranscriptConfidence: 0.9,
1481
+ is_finished: true
1482
+ });
1483
+
1484
+ stateChangeCallback.mockClear();
1485
+
1486
+ // Error after terminal - should be blocked
1487
+ onErrorCallback({
1488
+ audioUtteranceId: clientUuid,
1489
+ message: 'Late error'
1490
+ });
1491
+
1492
+ expect(stateChangeCallback).not.toHaveBeenCalled();
1493
+ });
1494
+
1495
+ it('should preserve original state after blocking duplicate', () => {
1496
+ // First terminal with specific transcript
1497
+ onTranscriptCallback({
1498
+ type: 'Transcription',
1499
+ audioUtteranceId: clientUuid,
1500
+ finalTranscript: 'original',
1501
+ finalTranscriptConfidence: 0.85,
1502
+ is_finished: true
1503
+ });
1504
+
1505
+ // Attempt second terminal with different transcript
1506
+ onTranscriptCallback({
1507
+ type: 'Transcription',
1508
+ audioUtteranceId: clientUuid,
1509
+ finalTranscript: 'different',
1510
+ finalTranscriptConfidence: 0.99,
1511
+ is_finished: true
1512
+ });
1513
+
1514
+ // State should still have original values
1515
+ const state = simplifiedClient.getVGFState();
1516
+ expect(state.finalTranscript).toBe('original');
1517
+ expect(state.finalConfidence).toBe(0.85);
1518
+ });
1519
+ });
1334
1520
  });
@@ -24,10 +24,10 @@ import {
24
24
  createVGFStateFromConfig,
25
25
  mapTranscriptionResultToState,
26
26
  mapErrorToState,
27
- updateStateOnStop
27
+ updateStateOnStop,
28
+ resetSessionState
28
29
  } from './vgf-recognition-mapper.js';
29
30
  import { RecognitionContextTypeV1 } from '@recog/shared-types';
30
- import { v4 as uuidv4 } from 'uuid';
31
31
 
32
32
  /**
33
33
  * Configuration for SimplifiedVGFRecognitionClient
@@ -153,6 +153,7 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
153
153
  private stateChangeCallback: ((state: RecognitionState) => void) | undefined;
154
154
  private expectedUuid: string;
155
155
  private logger: IRecognitionClientConfig['logger'];
156
+ private lastSentTerminalUuid: string | null = null;
156
157
 
157
158
  constructor(config: SimplifiedVGFClientConfig) {
158
159
  const { onStateChange, initialState, ...clientConfig } = config;
@@ -169,8 +170,9 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
169
170
  initialState.transcriptionStatus === TranscriptionStatus.ERROR ||
170
171
  (initialState.recognitionActionProcessingState !== undefined && initialState.recognitionActionProcessingState !== RecognitionActionProcessingState.COMPLETED);
171
172
  if (needsNewUuid) {
172
- // Generate new UUID for fresh session
173
- const newUUID = uuidv4();
173
+ // Reset session state with new UUID
174
+ this.state = resetSessionState(initialState);
175
+ const newUUID = this.state.audioUtteranceId;
174
176
 
175
177
  if (clientConfig.logger) {
176
178
  const reason = !initialState.audioUtteranceId ? 'Missing UUID' :
@@ -179,19 +181,12 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
179
181
  clientConfig.logger('info', `${reason} detected, generating new UUID: ${newUUID}`);
180
182
  }
181
183
 
182
- // Update state with new UUID and reset session-specific fields
183
- this.state = {
184
- ...initialState,
185
- audioUtteranceId: newUUID,
186
- transcriptionStatus: TranscriptionStatus.NOT_STARTED,
187
- startRecordingStatus: RecordingStatus.READY,
188
- recognitionActionProcessingState: RecognitionActionProcessingState.NOT_STARTED,
189
- finalTranscript: undefined
190
- };
191
-
192
184
  // Use new UUID in client config
193
185
  clientConfig.audioUtteranceId = newUUID;
194
186
 
187
+ // Reset terminal status tracking for new session
188
+ this.lastSentTerminalUuid = null;
189
+
195
190
  // Notify state change immediately so app can update
196
191
  if (onStateChange) {
197
192
  onStateChange(this.state);
@@ -245,12 +240,19 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
245
240
  if (result.audioUtteranceId && result.audioUtteranceId !== this.expectedUuid) {
246
241
  if (this.logger) {
247
242
  this.logger('warn',
248
- `[VGF] Skipping transcript update: UUID mismatch (expected: ${this.expectedUuid}, got: ${result.audioUtteranceId})`
243
+ `[RecogSDK:VGF] Skipping transcript update: UUID mismatch (expected: ${this.expectedUuid}, got: ${result.audioUtteranceId})`
249
244
  );
250
245
  }
251
- // Still call original callback if provided
252
- if (clientConfig.onTranscript) {
253
- clientConfig.onTranscript(result);
246
+ return;
247
+ }
248
+
249
+ // Skip if terminal status already sent for THIS session
250
+ // (If lastSentTerminalUuid exists but doesn't match expectedUuid, treat as new session)
251
+ if (this.lastSentTerminalUuid === this.expectedUuid) {
252
+ if (this.logger) {
253
+ this.logger('info',
254
+ `[RecogSDK:VGF] Duplicate terminal status suppressed (lastSentTerminalUuid: ${this.lastSentTerminalUuid})`
255
+ );
254
256
  }
255
257
  return;
256
258
  }
@@ -270,7 +272,7 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
270
272
  if (metadata.audioUtteranceId && metadata.audioUtteranceId !== this.expectedUuid) {
271
273
  if (this.logger) {
272
274
  this.logger('warn',
273
- `[VGF] Skipping metadata update: UUID mismatch (expected: ${this.expectedUuid}, got: ${metadata.audioUtteranceId})`
275
+ `[RecogSDK:VGF] Skipping metadata update: UUID mismatch (expected: ${this.expectedUuid}, got: ${metadata.audioUtteranceId})`
274
276
  );
275
277
  }
276
278
  return;
@@ -293,12 +295,17 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
293
295
  if (error.audioUtteranceId && error.audioUtteranceId !== this.expectedUuid) {
294
296
  if (this.logger) {
295
297
  this.logger('warn',
296
- `[VGF] Skipping error update: UUID mismatch (expected: ${this.expectedUuid}, got: ${error.audioUtteranceId})`
298
+ `[RecogSDK:VGF] Skipping error update: UUID mismatch (expected: ${this.expectedUuid}, got: ${error.audioUtteranceId})`
297
299
  );
298
300
  }
299
301
  return;
300
302
  }
301
303
 
304
+ // Skip if terminal status already sent for THIS session
305
+ if (this.lastSentTerminalUuid === this.expectedUuid) {
306
+ return;
307
+ }
308
+
302
309
  this.isRecordingAudio = false; // Reset on error
303
310
  this.state = mapErrorToState(this.state, error);
304
311
  this.notifyStateChange();
@@ -350,6 +357,27 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
350
357
  this.isRecordingAudio = false;
351
358
  this.state = updateStateOnStop(this.state);
352
359
  this.notifyStateChange();
360
+
361
+ // Early termination: If transcription never started (NOT_STARTED), emit synthetic finalization immediately
362
+ // This prevents games from getting stuck waiting for a server response that may never come
363
+ if (this.state.transcriptionStatus === TranscriptionStatus.NOT_STARTED) {
364
+ if (this.logger) {
365
+ this.logger('info',
366
+ `[RecogSDK:VGF] Early termination detected (transcriptionStatus: NOT_STARTED) - emitting synthetic finalization`
367
+ );
368
+ }
369
+ this.state = {
370
+ ...this.state,
371
+ transcriptionStatus: TranscriptionStatus.FINALIZED,
372
+ finalTranscript: '',
373
+ finalConfidence: 0,
374
+ pendingTranscript: '',
375
+ pendingConfidence: undefined,
376
+ finalTranscriptionTimestamp: new Date().toISOString()
377
+ };
378
+ this.notifyStateChange();
379
+ }
380
+
353
381
  await this.client.stopRecording();
354
382
  }
355
383
 
@@ -424,11 +452,34 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
424
452
  return { ...this.state };
425
453
  }
426
454
 
455
+ private isTerminalStatus(status: string | undefined): boolean {
456
+ return status === TranscriptionStatus.FINALIZED ||
457
+ status === TranscriptionStatus.ABORTED ||
458
+ status === TranscriptionStatus.ERROR;
459
+ }
460
+
427
461
  private notifyStateChange(): void {
428
- // State has already been validated for correct UUID before this is called
429
- if (this.stateChangeCallback) {
430
- this.stateChangeCallback({ ...this.state });
462
+
463
+ // Block duplicate terminal status emissions for THIS session
464
+ if (this.isTerminalStatus(this.state.transcriptionStatus)) {
465
+ if (this.lastSentTerminalUuid === this.expectedUuid) {
466
+ // Already sent a terminal status for this session - suppress duplicate
467
+ if (this.logger) {
468
+ this.logger('info',
469
+ `[RecogSDK:VGF] Duplicate terminal status suppressed (lastSentTerminalUuid: ${this.lastSentTerminalUuid})`
470
+ );
471
+ }
472
+ return;
473
+ }
474
+ // First terminal status for this session - record it
475
+ this.lastSentTerminalUuid = this.expectedUuid;
431
476
  }
477
+
478
+ if (!this.stateChangeCallback) {
479
+ return;
480
+ }
481
+
482
+ this.stateChangeCallback({ ...this.state });
432
483
  }
433
484
  }
434
485
 
@@ -477,4 +528,4 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
477
528
  */
478
529
  export function createSimplifiedVGFClient(config: SimplifiedVGFClientConfig): ISimplifiedVGFRecognitionClient {
479
530
  return new SimplifiedVGFRecognitionClient(config);
480
- }
531
+ }
@@ -55,7 +55,7 @@ export class AudioRingBuffer {
55
55
 
56
56
  // Log buffer overflow event
57
57
  if (this.logger) {
58
- this.logger('debug', 'Buffer overflow detected', {
58
+ this.logger('debug', '[RecogSDK] Buffer overflow detected', {
59
59
  bufferSize: this.bufferSize,
60
60
  totalOverflows: this.overflowCount,
61
61
  droppedChunk: this.buffer[this.readIndex]?.timestamp
@@ -151,7 +151,7 @@ export class AudioRingBuffer {
151
151
  this.totalBufferedBytes = 0;
152
152
 
153
153
  if (this.logger) {
154
- this.logger('debug', 'Audio buffer cleared');
154
+ this.logger('debug', '[RecogSDK] Audio buffer cleared');
155
155
  }
156
156
  }
157
157
 
@@ -44,7 +44,7 @@ export class MessageHandler {
44
44
  handleMessage(msg: { v: number; type: string; data: any }): void {
45
45
  // Log ALL incoming messages for debugging
46
46
  if (this.callbacks.logger) {
47
- this.callbacks.logger('debug', 'Received WebSocket message', {
47
+ this.callbacks.logger('debug', '[RecogSDK] Received WebSocket message', {
48
48
  msgType: msg.type,
49
49
  msgDataType: msg.data && typeof msg.data === 'object' && 'type' in msg.data ? msg.data.type : 'N/A',
50
50
  fullMessage: msg
@@ -55,7 +55,7 @@ export class MessageHandler {
55
55
  // Log error if we receive primitive data (indicates server issue)
56
56
  if (msg.data && typeof msg.data !== 'object') {
57
57
  if (this.callbacks.logger) {
58
- this.callbacks.logger('error', 'Received primitive msg.data from server', {
58
+ this.callbacks.logger('error', '[RecogSDK] Received primitive msg.data from server', {
59
59
  dataType: typeof msg.data,
60
60
  data: msg.data,
61
61
  fullMessage: msg
@@ -90,7 +90,7 @@ export class MessageHandler {
90
90
  default:
91
91
  // Unknown message type - log if logger available
92
92
  if (this.callbacks.logger) {
93
- this.callbacks.logger('debug', 'Unknown message type', { type: msgType });
93
+ this.callbacks.logger('debug', '[RecogSDK] Unknown message type', { type: msgType });
94
94
  }
95
95
  }
96
96
  }
@@ -106,7 +106,7 @@ export class MessageHandler {
106
106
  const timeToFirstTranscript = this.firstTranscriptTime - this.sessionStartTime;
107
107
 
108
108
  if (this.callbacks.logger) {
109
- this.callbacks.logger('debug', 'First transcript received', {
109
+ this.callbacks.logger('debug', '[RecogSDK] First transcript received', {
110
110
  timeToFirstTranscriptMs: timeToFirstTranscript
111
111
  });
112
112
  }
@@ -19,6 +19,8 @@ export interface UrlBuilderConfig {
19
19
  questionAnswerId?: string;
20
20
  platform?: string;
21
21
  gameContext?: GameContextV1;
22
+ /** Standalone gameId - takes precedence over gameContext.gameId if both provided */
23
+ gameId?: string;
22
24
  }
23
25
 
24
26
  /**
@@ -75,9 +77,14 @@ export function buildWebSocketUrl(config: UrlBuilderConfig): string {
75
77
  url.searchParams.set('platform', config.platform);
76
78
  }
77
79
 
78
- // Add gameId and gamePhase if provided from gameContext
79
- if (config.gameContext) {
80
- url.searchParams.set('gameId', config.gameContext.gameId);
80
+ // Add gameId - standalone gameId takes precedence over gameContext.gameId
81
+ const gameId = config.gameId ?? config.gameContext?.gameId;
82
+ if (gameId) {
83
+ url.searchParams.set('gameId', gameId);
84
+ }
85
+
86
+ // Add gamePhase from gameContext if provided
87
+ if (config.gameContext?.gamePhase) {
81
88
  url.searchParams.set('gamePhase', config.gameContext.gamePhase);
82
89
  }
83
90
 
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Unit tests for VGF Recognition Mapper functions
3
+ */
4
+
5
+ import {
6
+ resetSessionState
7
+ } from './vgf-recognition-mapper.js';
8
+ import {
9
+ RecognitionState,
10
+ RecordingStatus,
11
+ TranscriptionStatus,
12
+ RecognitionActionProcessingState
13
+ } from './vgf-recognition-state.js';
14
+
15
+ describe('resetSessionState', () => {
16
+ it('should generate a new UUID', () => {
17
+ const originalState: RecognitionState = {
18
+ audioUtteranceId: 'old-uuid-123',
19
+ pendingTranscript: '',
20
+ transcriptionStatus: TranscriptionStatus.FINALIZED,
21
+ startRecordingStatus: RecordingStatus.FINISHED,
22
+ recognitionActionProcessingState: RecognitionActionProcessingState.COMPLETED,
23
+ finalTranscript: 'hello world'
24
+ };
25
+
26
+ const newState = resetSessionState(originalState);
27
+
28
+ expect(newState.audioUtteranceId).not.toBe('old-uuid-123');
29
+ expect(newState.audioUtteranceId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
30
+ });
31
+
32
+ it('should reset session-specific fields', () => {
33
+ const originalState: RecognitionState = {
34
+ audioUtteranceId: 'old-uuid-123',
35
+ pendingTranscript: 'partial',
36
+ transcriptionStatus: TranscriptionStatus.FINALIZED,
37
+ startRecordingStatus: RecordingStatus.FINISHED,
38
+ recognitionActionProcessingState: RecognitionActionProcessingState.COMPLETED,
39
+ finalTranscript: 'hello world'
40
+ };
41
+
42
+ const newState = resetSessionState(originalState);
43
+
44
+ expect(newState.transcriptionStatus).toBe(TranscriptionStatus.NOT_STARTED);
45
+ expect(newState.startRecordingStatus).toBe(RecordingStatus.READY);
46
+ expect(newState.recognitionActionProcessingState).toBe(RecognitionActionProcessingState.NOT_STARTED);
47
+ expect(newState.finalTranscript).toBeUndefined();
48
+ });
49
+
50
+ it('should preserve non-session fields like promptSlotMap', () => {
51
+ const originalState: RecognitionState = {
52
+ audioUtteranceId: 'old-uuid-123',
53
+ pendingTranscript: '',
54
+ transcriptionStatus: TranscriptionStatus.FINALIZED,
55
+ promptSlotMap: { artist: ['taylor swift'], song: ['shake it off'] },
56
+ asrConfig: '{"provider":"deepgram"}'
57
+ };
58
+
59
+ const newState = resetSessionState(originalState);
60
+
61
+ expect(newState.promptSlotMap).toEqual({ artist: ['taylor swift'], song: ['shake it off'] });
62
+ expect(newState.asrConfig).toBe('{"provider":"deepgram"}');
63
+ });
64
+
65
+ it('should work with minimal state', () => {
66
+ const originalState: RecognitionState = {
67
+ audioUtteranceId: 'old-uuid',
68
+ pendingTranscript: ''
69
+ };
70
+
71
+ const newState = resetSessionState(originalState);
72
+
73
+ expect(newState.audioUtteranceId).not.toBe('old-uuid');
74
+ expect(newState.transcriptionStatus).toBe(TranscriptionStatus.NOT_STARTED);
75
+ expect(newState.startRecordingStatus).toBe(RecordingStatus.READY);
76
+ expect(newState.recognitionActionProcessingState).toBe(RecognitionActionProcessingState.NOT_STARTED);
77
+ });
78
+ });
@@ -9,8 +9,10 @@ import {
9
9
  RecognitionState,
10
10
  RecordingStatus,
11
11
  TranscriptionStatus,
12
+ RecognitionActionProcessingState,
12
13
  createInitialRecognitionState
13
14
  } from './vgf-recognition-state.js';
15
+ import { v4 as uuidv4 } from 'uuid';
14
16
  import {
15
17
  ClientState,
16
18
  IRecognitionClientConfig
@@ -142,6 +144,33 @@ export function updateStateOnStop(currentState: RecognitionState): RecognitionSt
142
144
  };
143
145
  }
144
146
 
147
+ /**
148
+ * Resets session state with a new UUID.
149
+ *
150
+ * This creates a fresh session state while preserving non-session fields
151
+ * (like promptSlotMap, asrConfig, etc.)
152
+ *
153
+ * Resets:
154
+ * - audioUtteranceId → new UUID
155
+ * - transcriptionStatus → NOT_STARTED
156
+ * - startRecordingStatus → READY
157
+ * - recognitionActionProcessingState → NOT_STARTED
158
+ * - finalTranscript → undefined
159
+ *
160
+ * @param currentState - The current recognition state
161
+ * @returns A new state with reset session fields and a new UUID
162
+ */
163
+ export function resetSessionState(currentState: RecognitionState): RecognitionState {
164
+ return {
165
+ ...currentState,
166
+ audioUtteranceId: uuidv4(),
167
+ transcriptionStatus: TranscriptionStatus.NOT_STARTED,
168
+ startRecordingStatus: RecordingStatus.READY,
169
+ recognitionActionProcessingState: RecognitionActionProcessingState.NOT_STARTED,
170
+ finalTranscript: undefined
171
+ };
172
+ }
173
+
145
174
  /**
146
175
  * Updates state when client becomes ready
147
176
  */