dsh-live-voice 0.7.1 → 0.7.2

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.
@@ -36682,7 +36682,7 @@ async function createQwenRuntime(options) {
36682
36682
  }
36683
36683
 
36684
36684
  // src/modules/recognition/local/assets.ts
36685
- var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.1" : "test") + "/";
36685
+ var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
36686
36686
 
36687
36687
  // src/modules/recognition/local/modelCatalog.ts
36688
36688
  var browserAsrModels = [
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.1",
2
+ "version": "0.7.2",
3
3
  "assets": [
4
4
  "asr.worker.js",
5
5
  "manifest.json",
package/lib/client.js CHANGED
@@ -152,7 +152,7 @@ var SessionSpeechMemory = class {
152
152
  };
153
153
 
154
154
  // src/modules/recognition/vad/assets.ts
155
- var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.1" : "test") + "/";
155
+ var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
156
156
  async function sileroVadAvailable(fetchImpl = globalThis.fetch) {
157
157
  try {
158
158
  const response = await fetchImpl(sileroVadAssetBase() + "availability", {
@@ -3507,7 +3507,7 @@ async function removeBrowserModel(key, storage = caches) {
3507
3507
  }
3508
3508
 
3509
3509
  // src/modules/recognition/local/assets.ts
3510
- var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.1" : "test") + "/";
3510
+ var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
3511
3511
 
3512
3512
  // src/modules/recognition/local/runtimeIdentity.ts
3513
3513
  var workerProtocol = "asr-worker-v3-ort-1.31.0-dev.20260914-8d85527a0";
@@ -10662,9 +10662,9 @@ var VoiceCoordinator = class _VoiceCoordinator {
10662
10662
  return await this.recognition.suspendCapture() !== false;
10663
10663
  });
10664
10664
  }
10665
- async _resumeInputCapture() {
10665
+ async _resumeInputCapture(valid = () => true) {
10666
10666
  return this._input(async () => {
10667
- if (!this.snapshot.listening || typeof this.recognition.resumeCapture !== "function")
10667
+ if (!valid() || !this.snapshot.listening || typeof this.recognition.resumeCapture !== "function")
10668
10668
  return false;
10669
10669
  return await this.recognition.resumeCapture() !== false;
10670
10670
  });
@@ -11458,22 +11458,53 @@ var VoiceCoordinator = class _VoiceCoordinator {
11458
11458
  getSpeechProgress() {
11459
11459
  return this.engines[this.snapshot.speechEngine]?.getPlaybackProgress?.() ?? null;
11460
11460
  }
11461
+ async _restorePausedSpeechInput(epoch, control) {
11462
+ const valid = () => !this.disposed && !this.navigationPaused && epoch === this.speechEpoch && control === this.controlEpoch && this.snapshot.paused && this.snapshot.settings.mode === "speaker";
11463
+ if (!valid()) return;
11464
+ try {
11465
+ if (this._hasSuspendedInputCapture()) {
11466
+ await this._resumeInputCapture(valid);
11467
+ } else if (this.snapshot.conversation && !this.snapshot.listening && !this.snapshot.starting && !this.snapshot.inputRecovery) {
11468
+ await this._startInput(true);
11469
+ }
11470
+ } catch (error) {
11471
+ if (valid()) this.patch({ inputError: message(error), error: message(error) });
11472
+ }
11473
+ }
11474
+ async _gateResumedSpeechInput() {
11475
+ if (this.snapshot.settings.mode !== "speaker") return;
11476
+ const suspended = await this._suspendInputCapture();
11477
+ if (!suspended) {
11478
+ await this.stopListening();
11479
+ if (this.inputReleaseError) throw this.inputReleaseError;
11480
+ }
11481
+ }
11461
11482
  async pauseSpeech(automatic = false) {
11462
11483
  if (this.snapshot.speechRecovery?.retrying) this._cancelRecovery("speech", true);
11463
11484
  if (!automatic) {
11464
11485
  this._cancelInterruptionResume();
11465
11486
  this.interruptionPausedSpeech = false;
11466
11487
  }
11467
- if (this.disposed || this.snapshot.paused) return;
11488
+ if (this.disposed) return;
11489
+ if (this.snapshot.paused) {
11490
+ const epoch2 = this.speechEpoch;
11491
+ const control2 = ++this.controlEpoch;
11492
+ await this.inputTail;
11493
+ await this._restorePausedSpeechInput(epoch2, control2);
11494
+ return;
11495
+ }
11468
11496
  if (!this.snapshot.speaking && this.snapshot.speechRunActive) {
11469
11497
  this._cancelAssistantSpeechTimer();
11498
+ const control2 = ++this.controlEpoch;
11470
11499
  this.patch({ paused: true });
11500
+ await this._restorePausedSpeechInput(this.speechEpoch, control2);
11471
11501
  return;
11472
11502
  }
11473
11503
  if (!this.snapshot.speaking) return;
11474
11504
  if (this.speechLaunchPending) {
11475
- ++this.controlEpoch;
11505
+ const control2 = ++this.controlEpoch;
11476
11506
  this.patch({ paused: true });
11507
+ await this._restorePausedSpeechInput(this.speechEpoch, control2);
11477
11508
  return;
11478
11509
  }
11479
11510
  const epoch = this.speechEpoch;
@@ -11484,6 +11515,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
11484
11515
  if (epoch === this.speechEpoch && control === this.controlEpoch && this.snapshot.speaking && result !== false) {
11485
11516
  this.patch({ paused: true });
11486
11517
  this.activeRecordingTimeline?.("assistant-pause");
11518
+ await this._restorePausedSpeechInput(epoch, control);
11487
11519
  }
11488
11520
  } catch (error) {
11489
11521
  if (epoch === this.speechEpoch) this.patch({ error: message(error) });
@@ -11503,12 +11535,11 @@ var VoiceCoordinator = class _VoiceCoordinator {
11503
11535
  if (this.speechLaunchPending) {
11504
11536
  const epoch2 = this.speechEpoch;
11505
11537
  const control2 = ++this.controlEpoch;
11506
- if (this.snapshot.settings.mode === "speaker") {
11507
- await this.stopListening();
11508
- if (this.inputReleaseError) {
11509
- this.patch({ error: message(this.inputReleaseError) });
11510
- return;
11511
- }
11538
+ try {
11539
+ await this._gateResumedSpeechInput();
11540
+ } catch (error) {
11541
+ if (epoch2 === this.speechEpoch) this.patch({ error: message(error) });
11542
+ return;
11512
11543
  }
11513
11544
  if (epoch2 === this.speechEpoch && control2 === this.controlEpoch)
11514
11545
  this.patch({ paused: false });
@@ -11517,19 +11548,19 @@ var VoiceCoordinator = class _VoiceCoordinator {
11517
11548
  const epoch = this.speechEpoch;
11518
11549
  const control = ++this.controlEpoch;
11519
11550
  try {
11520
- if (this.snapshot.settings.mode === "speaker" && !this._hasSuspendedInputCapture()) {
11521
- await this.stopListening();
11522
- if (this.inputReleaseError) throw this.inputReleaseError;
11523
- }
11551
+ await this._gateResumedSpeechInput();
11524
11552
  if (epoch !== this.speechEpoch || control !== this.controlEpoch) return;
11525
11553
  const result = await this.engines[this.snapshot.speechEngine || this.snapshot.settings.engine].resume();
11526
11554
  if (epoch === this.speechEpoch && control === this.controlEpoch && result !== false) {
11527
11555
  this.patch({ paused: false });
11528
11556
  this.activeRecordingTimeline?.("assistant-resume");
11529
11557
  this._prefetchSpeech();
11558
+ } else {
11559
+ await this._restorePausedSpeechInput(epoch, control);
11530
11560
  }
11531
11561
  } catch (error) {
11532
11562
  if (epoch === this.speechEpoch) this.patch({ error: message(error) });
11563
+ await this._restorePausedSpeechInput(epoch, control);
11533
11564
  }
11534
11565
  }
11535
11566
  async speak(text, messageId = null) {
@@ -11624,6 +11655,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
11624
11655
  if (gating) timeline?.("input-stop-complete");
11625
11656
  }
11626
11657
  if (epoch !== this.speechEpoch || this.disposed) return;
11658
+ if (this.snapshot.paused) await this._restorePausedSpeechInput(epoch, this.controlEpoch);
11627
11659
  const queued = item;
11628
11660
  if (queued?.preparing) await queued.preparing;
11629
11661
  if (epoch !== this.speechEpoch || this.disposed) return;
@@ -14769,7 +14801,7 @@ function createDeviceOptions(devices, kind, systemDefault, fallback) {
14769
14801
  var import_react39 = __toESM(require("react"), 1);
14770
14802
 
14771
14803
  // src/modules/settings/services/releases.ts
14772
- var CURRENT_VERSION = "0.7.1";
14804
+ var CURRENT_VERSION = "0.7.2";
14773
14805
  var TESTED_DSH_VERSION = "0.2.2-rc.1";
14774
14806
  var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
14775
14807
  var RELEASES_URL = `${REPOSITORY_URL}/releases`;
@@ -18081,7 +18113,7 @@ function apply(ctx) {
18081
18113
  entry.chatListeners.add(listener);
18082
18114
  return () => entry.chatListeners.delete(listener);
18083
18115
  };
18084
- entry.readChat = entry.chat.getSnapshot.bind(entry.chat);
18116
+ entry.readChat = () => entry.chat.getSnapshot();
18085
18117
  const refresh = (reset = false) => {
18086
18118
  if (disposed || entry.closed) return;
18087
18119
  const snapshot = entry.readChat();
@@ -18109,10 +18141,20 @@ function apply(ctx) {
18109
18141
  };
18110
18142
  entry.refreshSpeech = refresh;
18111
18143
  refresh();
18112
- entry.unsubscribe = entry.chat.subscribe(() => {
18144
+ const publishChat = () => {
18113
18145
  refresh();
18114
18146
  for (const listener of entry.chatListeners) listener();
18115
- });
18147
+ };
18148
+ entry.unsubscribe = entry.chat.subscribe(publishChat);
18149
+ entry.reconnectChat = () => {
18150
+ if (disposed || entry.closed) return;
18151
+ const chat = ctx.uiConversation.binding(sessionId).target("chat");
18152
+ if (chat === entry.chat) return;
18153
+ entry.unsubscribe?.();
18154
+ entry.chat = chat;
18155
+ entry.unsubscribe = chat.subscribe(publishChat);
18156
+ publishChat();
18157
+ };
18116
18158
  const pendingInteractions = ctx.uiSession.pendingInteractions;
18117
18159
  const refreshPendingQuestion = (baseline = false) => {
18118
18160
  if (disposed || entry.closed || !pendingInteractions) return;
@@ -18289,6 +18331,7 @@ function apply(ctx) {
18289
18331
  import_react75.default.useLayoutEffect(() => {
18290
18332
  if (disposed) return;
18291
18333
  const current = get(sessionId);
18334
+ current.reconnectChat();
18292
18335
  current.refs++;
18293
18336
  if (kind === "buttons") current.buttons++;
18294
18337
  setEntry(current);
package/lib/server.js CHANGED
@@ -2052,7 +2052,7 @@ var VAD_ASSET_TYPES = {
2052
2052
  };
2053
2053
  var MAX_VAD_ASSET_BYTES = 32 * 1024 * 1024;
2054
2054
  function registerVadAssetRoutes(ctx, options = {}) {
2055
- const version = options.version ?? (true ? "0.7.1" : "test");
2055
+ const version = options.version ?? (true ? "0.7.2" : "test");
2056
2056
  if (!/^[a-zA-Z0-9][a-zA-Z0-9.+-]{0,127}$/.test(version))
2057
2057
  throw new Error("Invalid VAD asset version");
2058
2058
  const directory = options.directory ?? fileURLToPath(new URL("./vad/", import.meta.url));
@@ -2146,7 +2146,7 @@ import { lstat as lstat2, readFile as readFile5 } from "node:fs/promises";
2146
2146
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2147
2147
  import { join as join8 } from "node:path";
2148
2148
  function registerBrowserAsrAssets(ctx, options = {}) {
2149
- const version = options.version ?? (true ? "0.7.1" : "test");
2149
+ const version = options.version ?? (true ? "0.7.2" : "test");
2150
2150
  if (!/^[a-zA-Z0-9][a-zA-Z0-9.+-]{0,127}$/.test(version)) throw new Error("invalid-version");
2151
2151
  const base = "/api/dsh-live-voice/asr/" + encodeURIComponent(version) + "/";
2152
2152
  const directory = options.directory ?? fileURLToPath2(new URL("./asr/", import.meta.url));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "pluginVersion": "0.7.1",
3
+ "pluginVersion": "0.7.2",
4
4
  "dependencies": {
5
5
  "@ricky0123/vad-web": "0.0.31",
6
6
  "onnxruntime-web": "1.22.0"
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "vad.worker.js": {
43
43
  "bytes": 79337,
44
- "sha256": "36a94fd4b60d3717d25628a033574d1b6c2fb576a8be732c87255ba567c33bd0"
44
+ "sha256": "6b83547e105d6a61b37f3647687021afcab2162d707071e7bffd9a6ffecade58"
45
45
  }
46
46
  }
47
47
  }
@@ -1632,7 +1632,7 @@ var vo = Ke;
1632
1632
  Object.defineProperty(I.versions, "web", { value: $t, enumerable: true });
1633
1633
 
1634
1634
  // src/modules/recognition/vad/assets.ts
1635
- var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.1" : "test") + "/";
1635
+ var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
1636
1636
 
1637
1637
  // src/modules/recognition/vad/StreamingResampler.ts
1638
1638
  var StreamingResampler = class {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live-voice",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "dshTestedVersion": "0.2.2-rc.1",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -3,6 +3,7 @@
3
3
  `src/app` is the composition boundary between DSH and the voice-domain modules. Client and server entry points remain separate; host-only Node dependencies must never enter the browser bundle.
4
4
 
5
5
  - `client/apply.tsx` injects DSH client services, creates session controllers and voice ownership, wires engines and settings, and disposes resources. `client/slotDefinitions.ts` and `client/registerSlots.tsx` own the established slot IDs, order, and registration. Preserve those contracts when changing placement. Microphone mode stop invalidates pending input ownership and application continuity without ending current playback; explicit takeover and full End retain their separate handoff/lifecycle semantics.
6
+ - Retained session entries re-resolve the current DSH chat target on committed slot attachment. A replaced target releases its old subscription and republishes through stable readers/listeners without resetting session speech memory; detached-session admission still prevents background playback.
6
7
  - Composer detachment parks eligible recording-backed microphone controllers in the existing session registry rather than retiring them. Their capture clock pauses, recognition requests continue, and final results stay on the originating controller. Returning uses the existing ownership barrier before resuming capture and delivering queued results against the latest draft. Explicit stop clears parked intent; plugin disposal retires both mounted and parked entries. This memory is page-lifetime only, not reload recovery.
7
8
  - `server/apply.ts` composes host engines and providers. `server/registerRoutes.ts` registers authenticated same-origin routes; do not bypass their validation or expose host access directly to browser code.
8
9
  - `client/transcriptionLeaveGuard.ts` owns one activity-scoped native `beforeunload` listener across microphone, meeting sources and Settings test capture. It covers acquisition, listening and unsettled transcription, not playback or voice-mode preference alone. Cancelling the browser dialog does not stop capture; existing `pagehide` cleanup runs only after leaving. Idle and disposed apps remove the listener. Browser wording, user-activation requirements and mobile/forced-close limitations apply; this is not refresh recovery.
@@ -690,7 +690,8 @@ export function apply(ctx) {
690
690
  entry.chatListeners.add(listener);
691
691
  return () => entry.chatListeners.delete(listener);
692
692
  };
693
- entry.readChat = entry.chat.getSnapshot.bind(entry.chat);
693
+ // Stable React store readers must follow a replacement DSH session binding.
694
+ entry.readChat = () => entry.chat.getSnapshot();
694
695
  const refresh = (reset = false) => {
695
696
  if (disposed || entry.closed) return;
696
697
  const snapshot = entry.readChat();
@@ -729,10 +730,22 @@ export function apply(ctx) {
729
730
  };
730
731
  entry.refreshSpeech = refresh;
731
732
  refresh();
732
- entry.unsubscribe = entry.chat.subscribe(() => {
733
+ const publishChat = () => {
733
734
  refresh();
734
735
  for (const listener of entry.chatListeners) listener();
735
- });
736
+ };
737
+ entry.unsubscribe = entry.chat.subscribe(publishChat);
738
+ entry.reconnectChat = () => {
739
+ if (disposed || entry.closed) return;
740
+ const chat = ctx.uiConversation.binding(sessionId).target('chat');
741
+ if (chat === entry.chat) return;
742
+ entry.unsubscribe?.();
743
+ entry.chat = chat;
744
+ entry.unsubscribe = chat.subscribe(publishChat);
745
+ // Subscribe first: activation may materialize the target synchronously.
746
+ // Existing speech memory and detached-session admission rules still apply.
747
+ publishChat();
748
+ };
736
749
  // DSH 0.1.6 may omit this legacy store. Keep the remaining voice controls
737
750
  // available while structured-question integration is unavailable.
738
751
  const pendingInteractions = ctx.uiSession.pendingInteractions;
@@ -927,6 +940,7 @@ export function apply(ctx) {
927
940
  // Allocation and subscriptions happen only for committed mounts. Suspended
928
941
  // or abandoned renders never acquire a session, probe engines, or retain it.
929
942
  const current = get(sessionId);
943
+ current.reconnectChat();
930
944
  current.refs++;
931
945
  if (kind === 'buttons') current.buttons++;
932
946
  setEntry(current);
@@ -408,9 +408,13 @@ export class VoiceCoordinator {
408
408
  return (await this.recognition.suspendCapture()) !== false;
409
409
  });
410
410
  }
411
- async _resumeInputCapture() {
411
+ async _resumeInputCapture(valid = () => true) {
412
412
  return this._input(async () => {
413
- if (!this.snapshot.listening || typeof this.recognition.resumeCapture !== 'function')
413
+ if (
414
+ !valid() ||
415
+ !this.snapshot.listening ||
416
+ typeof this.recognition.resumeCapture !== 'function'
417
+ )
414
418
  return false;
415
419
  return (await this.recognition.resumeCapture()) !== false;
416
420
  });
@@ -1370,23 +1374,67 @@ export class VoiceCoordinator {
1370
1374
  getSpeechProgress() {
1371
1375
  return this.engines[this.snapshot.speechEngine]?.getPlaybackProgress?.() ?? null;
1372
1376
  }
1377
+ async _restorePausedSpeechInput(epoch, control) {
1378
+ const valid = () =>
1379
+ !this.disposed &&
1380
+ !this.navigationPaused &&
1381
+ epoch === this.speechEpoch &&
1382
+ control === this.controlEpoch &&
1383
+ this.snapshot.paused &&
1384
+ this.snapshot.settings.mode === 'speaker';
1385
+ if (!valid()) return;
1386
+ try {
1387
+ if (this._hasSuspendedInputCapture()) {
1388
+ await this._resumeInputCapture(valid);
1389
+ } else if (
1390
+ this.snapshot.conversation &&
1391
+ !this.snapshot.listening &&
1392
+ !this.snapshot.starting &&
1393
+ !this.snapshot.inputRecovery
1394
+ ) {
1395
+ await this._startInput(true);
1396
+ }
1397
+ } catch (error) {
1398
+ if (valid()) this.patch({ inputError: message(error), error: message(error) });
1399
+ }
1400
+ }
1401
+ async _gateResumedSpeechInput() {
1402
+ if (this.snapshot.settings.mode !== 'speaker') return;
1403
+ // Preserve recording-backed capture; native engines retain safe stop/restart.
1404
+ const suspended = await this._suspendInputCapture();
1405
+ if (!suspended) {
1406
+ await this.stopListening();
1407
+ if (this.inputReleaseError) throw this.inputReleaseError;
1408
+ }
1409
+ }
1373
1410
  async pauseSpeech(automatic = false) {
1374
1411
  if (this.snapshot.speechRecovery?.retrying) this._cancelRecovery('speech', true);
1375
1412
  if (!automatic) {
1376
1413
  this._cancelInterruptionResume();
1377
1414
  this.interruptionPausedSpeech = false;
1378
1415
  }
1379
- if (this.disposed || this.snapshot.paused) return;
1416
+ if (this.disposed) return;
1417
+ if (this.snapshot.paused) {
1418
+ // Reaffirming pause invalidates a resume whose capture gate is still pending.
1419
+ const epoch = this.speechEpoch;
1420
+ const control = ++this.controlEpoch;
1421
+ await this.inputTail;
1422
+ await this._restorePausedSpeechInput(epoch, control);
1423
+ return;
1424
+ }
1380
1425
  if (!this.snapshot.speaking && this.snapshot.speechRunActive) {
1381
1426
  this._cancelAssistantSpeechTimer();
1427
+ const control = ++this.controlEpoch;
1382
1428
  this.patch({ paused: true });
1429
+ await this._restorePausedSpeechInput(this.speechEpoch, control);
1383
1430
  return;
1384
1431
  }
1385
1432
  if (!this.snapshot.speaking) return;
1386
1433
  if (this.speechLaunchPending) {
1387
1434
  // Preparation has no player to pause yet; retain an explicit launch gate.
1388
- ++this.controlEpoch;
1435
+ const control = ++this.controlEpoch;
1389
1436
  this.patch({ paused: true });
1437
+ await this._restorePausedSpeechInput(this.speechEpoch, control);
1390
1438
  return;
1391
1439
  }
1392
1440
  const epoch = this.speechEpoch;
@@ -1403,6 +1451,7 @@ export class VoiceCoordinator {
1403
1451
  ) {
1404
1452
  this.patch({ paused: true });
1405
1453
  this.activeRecordingTimeline?.('assistant-pause');
1454
+ await this._restorePausedSpeechInput(epoch, control);
1406
1455
  }
1407
1456
  } catch (error) {
1408
1457
  if (epoch === this.speechEpoch) this.patch({ error: message(error) });
@@ -1422,12 +1471,11 @@ export class VoiceCoordinator {
1422
1471
  if (this.speechLaunchPending) {
1423
1472
  const epoch = this.speechEpoch;
1424
1473
  const control = ++this.controlEpoch;
1425
- if (this.snapshot.settings.mode === 'speaker') {
1426
- await this.stopListening();
1427
- if (this.inputReleaseError) {
1428
- this.patch({ error: message(this.inputReleaseError) });
1429
- return;
1430
- }
1474
+ try {
1475
+ await this._gateResumedSpeechInput();
1476
+ } catch (error) {
1477
+ if (epoch === this.speechEpoch) this.patch({ error: message(error) });
1478
+ return;
1431
1479
  }
1432
1480
  if (epoch === this.speechEpoch && control === this.controlEpoch)
1433
1481
  this.patch({ paused: false });
@@ -1436,10 +1484,7 @@ export class VoiceCoordinator {
1436
1484
  const epoch = this.speechEpoch;
1437
1485
  const control = ++this.controlEpoch;
1438
1486
  try {
1439
- if (this.snapshot.settings.mode === 'speaker' && !this._hasSuspendedInputCapture()) {
1440
- await this.stopListening();
1441
- if (this.inputReleaseError) throw this.inputReleaseError;
1442
- }
1487
+ await this._gateResumedSpeechInput();
1443
1488
  if (epoch !== this.speechEpoch || control !== this.controlEpoch) return;
1444
1489
  const result =
1445
1490
  await this.engines[this.snapshot.speechEngine || this.snapshot.settings.engine].resume();
@@ -1447,9 +1492,12 @@ export class VoiceCoordinator {
1447
1492
  this.patch({ paused: false });
1448
1493
  this.activeRecordingTimeline?.('assistant-resume');
1449
1494
  this._prefetchSpeech();
1495
+ } else {
1496
+ await this._restorePausedSpeechInput(epoch, control);
1450
1497
  }
1451
1498
  } catch (error) {
1452
1499
  if (epoch === this.speechEpoch) this.patch({ error: message(error) });
1500
+ await this._restorePausedSpeechInput(epoch, control);
1453
1501
  }
1454
1502
  }
1455
1503
  async speak(text, messageId = null) {
@@ -1548,6 +1596,8 @@ export class VoiceCoordinator {
1548
1596
  if (gating) timeline?.('input-stop-complete');
1549
1597
  }
1550
1598
  if (epoch !== this.speechEpoch || this.disposed) return;
1599
+ // A pause can arrive while the initial capture gate is still settling.
1600
+ if (this.snapshot.paused) await this._restorePausedSpeechInput(epoch, this.controlEpoch);
1551
1601
  const queued = item;
1552
1602
  if (queued?.preparing) await queued.preparing;
1553
1603
  if (epoch !== this.speechEpoch || this.disposed) return;