interrupt method

Future<void> interrupt()

Barge-in. Idempotent (a genuine no-op when idle, and a concurrent second call targeting the SAME in-flight turn neither re-calls stop() nor re-arms the drain timer โ€” it just awaits that turn's terminal via _interruptingController). A second call that targets a DIFFERENT (later) turn proceeds normally โ€” see N1. Sets the interrupt flag, calls responder.stop() (caught if it throws), bounded-drains the reply stream, then awaits the driver's terminal. Never writes to the controller (ยง12 B3).

Implementation

Future<void> interrupt() async {
  final controller = _activeController;
  if (controller == null) return; // idle
  if (identical(_interruptingController, controller)) {
    // Concurrent interrupt() of the SAME turn: true no-op, just await the
    // in-flight terminal.
    final done = _turnDone;
    if (done != null) await done.future;
    return;
  }
  _interruptingController = controller;
  _interruptRequested = true;
  // Capture BEFORE the await responder.stop() below (F1): if the current
  // turn completes naturally while stop() is in flight and the caller
  // immediately starts turn B, reading _replyDrained/_turnDone AFTER the
  // await would attach this drain timer / await to turn B instead of
  // turn A, force-completing B's un-interrupted drain 5s later.
  final drained = _replyDrained;
  final done = _turnDone;
  try {
    await _responder.stop().timeout(drainTimeout);
  } on TimeoutException {
    gemmaLog(
      'VoiceSession: responder.stop() did not resolve within '
      '${drainTimeout.inSeconds}s during barge-in; proceeding to drain.',
      level: GemmaLogLevel.info,
    );
  } catch (e) {
    gemmaLog(
      'VoiceSession: responder.stop() threw during barge-in: $e',
      level: GemmaLogLevel.info,
    );
  }
  // Bounded drain: give the reply stream drainTimeout to end, else force it.
  if (drained != null) {
    _armBoundedDrain(
      drained,
      'barge-in drain exceeded ${drainTimeout.inSeconds}s',
    );
  }
  try {
    if (done != null) await done.future;
  } finally {
    // Only clear if THIS interrupt still owns the marker โ€” a later turn's
    // interrupt() may have overwritten it; leave that one intact (N1).
    if (identical(_interruptingController, controller)) {
      _interruptingController = null;
    }
  }
}