@luckydraw/cumulus 0.30.24 → 0.30.25

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.
@@ -4374,6 +4374,7 @@
4374
4374
  audioQueue: [], // queue of Float32Array PCM chunks
4375
4375
  audioPlaying: false, // currently playing audio
4376
4376
  audioDone: false, // server signaled all audio sent
4377
+ micRestartTimer: null, // pending mic restart, cancellable when PCM resumes
4377
4378
  // Voice loading state for Safari compatibility
4378
4379
  voicesLoaded: false,
4379
4380
  speakQueue: [],
@@ -4501,9 +4502,7 @@
4501
4502
  } else {
4502
4503
  // No speech detected — restart listening
4503
4504
  setPhase('idle');
4504
- setTimeout(function () {
4505
- if (state.active) startRecognition();
4506
- }, 300);
4505
+ scheduleMicRestart(300);
4507
4506
  }
4508
4507
  };
4509
4508
 
@@ -4513,15 +4512,11 @@
4513
4512
  // Normal — just restart
4514
4513
  if (state.active) {
4515
4514
  setPhase('idle');
4516
- setTimeout(function () {
4517
- if (state.active) startRecognition();
4518
- }, 500);
4515
+ scheduleMicRestart(500);
4519
4516
  }
4520
4517
  } else {
4521
4518
  label.textContent = 'Mic error: ' + event.error;
4522
- setTimeout(function () {
4523
- if (state.active) startRecognition();
4524
- }, 1000);
4519
+ scheduleMicRestart(1000);
4525
4520
  }
4526
4521
  };
4527
4522
 
@@ -4700,12 +4695,18 @@
4700
4695
  state.speaking = false;
4701
4696
  if (state.active) {
4702
4697
  setPhase('idle');
4703
- setTimeout(function () {
4704
- if (state.active) startRecognition();
4705
- }, 300);
4698
+ scheduleMicRestart(300);
4706
4699
  }
4707
4700
  }
4708
4701
 
4702
+ function scheduleMicRestart(delay) {
4703
+ if (state.micRestartTimer) clearTimeout(state.micRestartTimer);
4704
+ state.micRestartTimer = setTimeout(function () {
4705
+ state.micRestartTimer = null;
4706
+ if (state.active && state.phase !== 'speaking') startRecognition();
4707
+ }, delay);
4708
+ }
4709
+
4709
4710
  // ── Server-side TTS (Web Audio API playback) ──
4710
4711
  function ensureAudioCtx() {
4711
4712
  if (!state.audioCtx) {
@@ -4721,7 +4722,29 @@
4721
4722
  }
4722
4723
 
4723
4724
  function queuePCMAudio(arrayBuffer) {
4724
- if (!state.active || state.phase === 'listening') return;
4725
+ if (!state.active) return;
4726
+
4727
+ // Server is sending audio — it always wins over the mic. If the widget
4728
+ // transitioned to listening (or back to idle) prematurely, recover by
4729
+ // killing recognition and resuming the speaking phase. Without this,
4730
+ // any subsequent PCM is silently dropped and the mic gets stuck in an
4731
+ // idle/listen flicker loop.
4732
+ if (state.phase !== 'speaking') {
4733
+ console.log('[Voice] PCM arrived in phase=' + state.phase + ', recovering to speaking');
4734
+ if (state.recognition) {
4735
+ try { state.recognition.abort(); } catch (e) {}
4736
+ }
4737
+ clearWatchdog();
4738
+ // Cancel any pending mic-restart timer so it can't override us
4739
+ if (state.micRestartTimer) {
4740
+ clearTimeout(state.micRestartTimer);
4741
+ state.micRestartTimer = null;
4742
+ }
4743
+ // Reset audioDone — server is still sending, so a previously-seen
4744
+ // voice_audio_done signal must have been spurious (or stale from a
4745
+ // prior turn). Wait for the next legitimate one.
4746
+ state.audioDone = false;
4747
+ }
4725
4748
 
4726
4749
  // Strip 4-byte "VPCM" header
4727
4750
  var pcmData = new Int16Array(arrayBuffer, 4);
@@ -4785,9 +4808,7 @@
4785
4808
  state.audioDone = false;
4786
4809
  if (state.active) {
4787
4810
  setPhase('idle');
4788
- setTimeout(function () {
4789
- if (state.active) startRecognition();
4790
- }, 300);
4811
+ scheduleMicRestart(300);
4791
4812
  }
4792
4813
  }
4793
4814
 
@@ -4849,6 +4870,10 @@
4849
4870
  clearTimeout(state.voiceLoadTimer);
4850
4871
  state.voiceLoadTimer = null;
4851
4872
  }
4873
+ if (state.micRestartTimer) {
4874
+ clearTimeout(state.micRestartTimer);
4875
+ state.micRestartTimer = null;
4876
+ }
4852
4877
  state.speakQueue.length = 0;
4853
4878
  state.voicesLoaded = false;
4854
4879
  state.waitingForServerTTS = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckydraw/cumulus",
3
- "version": "0.30.24",
3
+ "version": "0.30.25",
4
4
  "description": "RLM-based CLI chat wrapper for Claude with external history context management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",