Voice: wake-word dies on every audio-graph rebuild — cancelInFlight calls cancel() not stopFeed() #28

Closed
opened 2026-05-12 19:06:04 +00:00 by KofTwentyTwo · 1 comment
KofTwentyTwo commented 2026-05-12 19:06:04 +00:00 (Migrated from github.com)

Summary

cancelInFlight permanently kills wake-word on every audio-graph rebuild. AppDelegate wires cancelInFlight → wakeWordDAG.cancel() (which calls streamCont.finish() — the stream is closed forever). AudioGraphOwner.teardown invokes cancelInFlight on every rebuild trigger (.deviceChange, .micRegrant, .ringOverflow, .aecFallback). After first rebuild, wake-word stops working until app restart.

Evidence

  • File: App/AppDelegate.swift:1192-1194 (confirmed at current HEAD 8d94c1c):
    await graphOwner.setCancelInFlight { [wakeWordDAG] in
        await wakeWordDAG.cancel()
    }
    
  • File: packages/Voice/Sources/Voice/WakeWord/WakeWordDAG.swift:137-142 — cancel() calls streamCont.finish()
  • File: packages/Voice/Sources/Voice/WakeWord/WakeWordDAG.swift:144-162 — stopFeed() exists specifically for this case; docstring warns of the exact bug pattern
  • File: App/AppDelegate.swift:1125-1133 — rebuild-completion consumer only dismisses AEC banner; never calls wakeWordDAG.start(ring: newRing)

Root cause / hypothesis

stopFeed() was added specifically to let cancelInFlight stop the feed Task WITHOUT finishing the stream; the mic-regrant startup bug was fixed (AudioGraphOwner.swift:340-354), but the AppDelegate wiring to stopFeed() was never updated. The rebuild completion path needs a second fix to re-arm the DAG against the new ring.

Recommended fix

Two-line fix in AppDelegate.swift:1192:

await graphOwner.setCancelInFlight { [wakeWordDAG] in
    await wakeWordDAG.stopFeed()  // was: cancel()
}

Plus re-arm in rebuild completion consumer at AppDelegate.swift:1125-1133:

if let newRing = await graphOwner.ringBuffer {
    await wakeWordDAG.start(ring: newRing)
}

Test

  • Behavioral: simulate .deviceChange rebuild trigger → assert wake-word stream still active and fed by new ring
  • Live UAT: plug headphones in mid-session → verify wake-word still triggers

Source

  • Audit: .planning/audit-2026-05-12/voice-audit.md (2026-05-12) — finding P0-1

Cross-references

  • Related: #11 (M-6 Voice) — natural landing site, but this is a critical pre-migration fix
  • Compounds: #29 (P1-1 OpenWakeWordSession unbounded growth) — masked today because DAG dies before it can leak
## Summary `cancelInFlight` permanently kills wake-word on every audio-graph rebuild. AppDelegate wires `cancelInFlight → wakeWordDAG.cancel()` (which calls `streamCont.finish()` — the stream is closed forever). `AudioGraphOwner.teardown` invokes `cancelInFlight` on every rebuild trigger (`.deviceChange`, `.micRegrant`, `.ringOverflow`, `.aecFallback`). After first rebuild, wake-word stops working until app restart. ## Evidence - File: `App/AppDelegate.swift:1192-1194` (confirmed at current HEAD `8d94c1c`): ```swift await graphOwner.setCancelInFlight { [wakeWordDAG] in await wakeWordDAG.cancel() } ``` - File: `packages/Voice/Sources/Voice/WakeWord/WakeWordDAG.swift:137-142` — `cancel()` calls `streamCont.finish()` - File: `packages/Voice/Sources/Voice/WakeWord/WakeWordDAG.swift:144-162` — `stopFeed()` exists specifically for this case; docstring warns of the exact bug pattern - File: `App/AppDelegate.swift:1125-1133` — rebuild-completion consumer only dismisses AEC banner; never calls `wakeWordDAG.start(ring: newRing)` ## Root cause / hypothesis `stopFeed()` was added specifically to let `cancelInFlight` stop the feed Task WITHOUT finishing the stream; the mic-regrant startup bug was fixed (`AudioGraphOwner.swift:340-354`), but the AppDelegate wiring to `stopFeed()` was never updated. The rebuild completion path needs a second fix to re-arm the DAG against the new ring. ## Recommended fix Two-line fix in `AppDelegate.swift:1192`: ```swift await graphOwner.setCancelInFlight { [wakeWordDAG] in await wakeWordDAG.stopFeed() // was: cancel() } ``` Plus re-arm in rebuild completion consumer at `AppDelegate.swift:1125-1133`: ```swift if let newRing = await graphOwner.ringBuffer { await wakeWordDAG.start(ring: newRing) } ``` ## Test - Behavioral: simulate `.deviceChange` rebuild trigger → assert wake-word stream still active and fed by new ring - Live UAT: plug headphones in mid-session → verify wake-word still triggers ## Source - Audit: `.planning/audit-2026-05-12/voice-audit.md` (2026-05-12) — finding P0-1 ## Cross-references - Related: #11 (M-6 Voice) — natural landing site, but this is a critical pre-migration fix - Compounds: #29 (P1-1 OpenWakeWordSession unbounded growth) — masked today because DAG dies before it can leak
KofTwentyTwo commented 2026-05-13 11:51:54 +00:00 (Migrated from github.com)

Closed by v0.1 cluster merges on 2026-05-13. Develop now at a8c7f1a0.

Closed by v0.1 cluster merges on 2026-05-13. Develop now at `a8c7f1a0`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KofTwentyTwo/Jarvis#28