A Kotlin Job does not own every native thread
When the user taps Stop, the ViewModel enters stopping and cancels the operation Job. The generator must also request SDK cancellation. Closing a conversation while native work is still using it risks invalid resource access. The adapter waits for the terminal callback before releasing the conversation.
if (!finished.isCompleted)
withContext(NonCancellable) {
conversation.cancelProcess()
runCatching { finished.await() }
}
This cleanup excerpt depends on the surrounding deferred/callback setup in LocalStoryGenerator. NonCancellable protects cleanup from the cancellation that triggered it; it does not mean native work should run forever. The preview depends on the SDK delivering a terminal callback. If that contract fails, the wait can stall. This is a known behavior to investigate on actual hardware, not a proven bounded-stop guarantee.
Keep the useful partial result
StoriesViewModel periodically checkpoints drafts and saves again on completion or cancellation. A stopped draft can be reviewed, accepted or discarded. Late callbacks must not write into a new attempt. The current callback checks the active coroutine and the generator serializes ownership; tests simulate delayed chunks to check that boundary.
Leaving the app stops active scene generation. Rotation is different: the retained ViewModel preserves ownership while the UI is recreated. Model download is treated separately and may continue while the process remains alive, but it has no guaranteed background scheduling. Process death destroys native handles; Room reconstructs the saved story only.
Practice matrix
Exercise Stop before first text, Stop after text, rapid retry, rotate while a draft streams, and leave the app. Use the fake generator to verify state transitions deterministically. Record native results only after real inference is available. Expected invariants: one active generation, no mixing chunks across attempts, a visible partial draft after stop, and no automatic inference restart after process death.
Explain a tradeoff: timing out the cleanup wait and immediately closing resources may free memory sooner but can violate native ownership. A production timeout needs a tested SDK recovery strategy, not simply another withTimeout around close. Keep the UI recoverable and expose a useful failure state instead of silently claiming stop succeeded.