fix(flow): improve tail capture and final chunk ASR recovery

- Add FlowUtteranceEndCoordinator with 350ms silence drain and 150ms post-roll
- Extend FinalChunkRecovery for short/empty final chunks in chunked pipeline
- Snapshot partial at mic stop and guard final transcript in FlowSessionManager
- Unify tail drain presets (iosFlow/macMLX) and expand diagnostics

Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-26 10:13:40 +00:00
parent e6f99d2744
commit c7ee891a90
16 changed files with 485 additions and 42 deletions
@@ -666,16 +666,11 @@ public final class FlowContinuousCapture {
gate.withLock { $0 = .draining }
drainTracker.beginDrain()
var endedBySilence = false
while true {
let decision = drainTracker.shouldFinish(policy: policy)
if decision.finished {
endedBySilence = decision.endedBySilence
break
}
if Task.isCancelled { break }
try? await Task.sleep(nanoseconds: FlowCaptureConstants.drainPollIntervalNs)
}
let timing = await FlowUtteranceEndCoordinator.awaitTailCapture(
tracker: drainTracker,
policy: policy,
pollIntervalNs: FlowCaptureConstants.drainPollIntervalNs
)
// NOTE: We intentionally do NOT signal `.endOfStream` to the shared
// downsampling converter here. `AVAudioConverter` is stateful: once its
@@ -692,8 +687,9 @@ public final class FlowContinuousCapture {
let tailSamples = tailSampleCounter.withLock { $0 }
let report = FlowCaptureDrainReport(
drainDurationSeconds: drainTracker.elapsedSeconds(),
endedBySilence: endedBySilence,
tailSampleCount: tailSamples
endedBySilence: timing.endedBySilence,
tailSampleCount: tailSamples,
postRollDurationSeconds: timing.postRollDurationSeconds
)
drainTracker.reset()
tailSampleCounter.withLock { $0 = 0 }