Store domain data, not runtime handles
StoryStore serializes a Story into a JSON payload in one Room row keyed by story ID. StoryRecord also keeps updated for sorting. This is simple for a small sample and supports flexible payload fields, but querying individual chapters is harder than a normalized chapter table. ignoreUnknownKeys tolerates extra JSON fields; it is not a complete schema-migration strategy.
@Entity(tableName = "stories")
data class StoryRecord(@PrimaryKey val id: String,
val payload: String, val updated: Long)
@Dao
interface StoryDao {
@Query("SELECT * FROM stories ORDER BY updated DESC")
fun observe(): Flow<List<StoryRecord>>
@Upsert suspend fun save(record: StoryRecord)
@Query("DELETE FROM stories WHERE id = :id")
suspend fun delete(id: String)
}
This excerpt uses Room and Flow imports from data/StoryStore.kt. A saved image is referenced by its private path; no engine, conversation or GPU buffer is serialized. On restart the bookshelf can recover even when the model is absent.
Acceptance is an explicit transition
keepDraft requires nonblank prose, appends Scene(draft, draftAction, draftImage), clears the transient fields and updates the timestamp. That transition protects context construction from treating unfinished text as canon. The ViewModel checkpoints roughly every 750 ms while generation runs and saves in cleanup. Process death can still lose the newest fraction of a second; this is not a promise to persist every token.
Photos and weights live in no-backup storage, and backup rules exclude app data. App-private storage is a sandbox boundary, not a claim of application-level encryption. A future share/export action must explicitly decide what leaves that boundary.
Practice and checkpoint
Use the fake generator tests to create a partial draft. Verify that accepting it once yields exactly one scene and clears transient state. Reopen a saved captured story without loading Gemma. Then terminate and relaunch the app; distinguish the retained Room data from a retained ViewModel during rotation. Do not use Clear storage for this test because that intentionally deletes the database.
Expected evidence: accepted text and its photo association survive relaunch, an unaccepted draft stays distinguishable, and no inference restarts silently. Consider failed file deletion: a database delete and file deletion are not one cross-filesystem transaction. A production cleanup worker should account for orphaned files rather than claiming atomic deletion of both.