interrupt method
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;
}
}
}