@@ -150,12 +150,13 @@ open class DraftStore private constructor(
150 if (text.isBlank()) remove(key) else putString(key, text)
150 if (text.isBlank()) remove(key) else putString(key, text)
151 }.apply()
151 }.apply()
152 }.onFailure { Log.w("DraftStore", "Failed to persist $key", it) }
152 }.onFailure { Log.w("DraftStore", "Failed to persist $key", it) }
154 // FIX: always submit to the single-thread flushExecutor (never apply directly) so
155 // FIX: always submit to the single-thread flushExecutor (never apply directly) so
155 // writes persist in submission order. Previously the post-init fast path applied
156 // writes persist in submission order. Previously the post-init fast path applied
156 // synchronously on the caller's thread, which could land BEFORE an earlier still-queued
157 // synchronously on the caller's thread, which could land BEFORE an earlier still-queued
157 // write, persisting stale text. The task resolves the prefs lazy and calls apply().
158 // write, persisting stale text. The task resolves the prefs lazy and calls apply().
158− runCatching { flushExecutor.execute { write() } }
159+ submitFlush(write, key)
161 /** Update the in-memory draft immediately without persisting to disk.
162 /** Update the in-memory draft immediately without persisting to disk.
@@ -202,13 +203,14 @@ open class DraftStore private constructor(
202 // Submit the (combined draft + follow-up) removal to the same single-thread
203 // Submit the (combined draft + follow-up) removal to the same single-thread
203 // flushExecutor the other writers use, so it can't race ahead of an earlier queued
204 // flushExecutor the other writers use, so it can't race ahead of an earlier queued
204 // write and resurrect stale text. Mirrors flushDraft's submission pattern.
205 // write and resurrect stale text. Mirrors flushDraft's submission pattern.
205− runCatching {
206+ submitFlush(
206− flushExecutor.execute {
207+ {
207 runCatching {
208 runCatching {
208 prefs.edit().remove(sessionId).remove(FOLLOWUP_PREFIX + sessionId).apply()
209 prefs.edit().remove(sessionId).remove(FOLLOWUP_PREFIX + sessionId).apply()
209 }.onFailure { Log.w("DraftStore", "Failed to remove draft for $sessionId", it) }
210 }.onFailure { Log.w("DraftStore", "Failed to remove draft for $sessionId", it) }
211− }
212+ "remove($sessionId)",
@@ -238,15 +240,31 @@ open class DraftStore private constructor(
238 // still-queued write completed, clobbering the draft with stale text. The executor task
240 // still-queued write completed, clobbering the draft with stale text. The executor task
239 // resolves the prefs lazy and uses apply() (async) exactly as before; the write may be
241 // resolves the prefs lazy and uses apply() (async) exactly as before; the write may be
240 // lost only if the process exits before the queued task runs (as previously).
242 // lost only if the process exits before the queued task runs (as previously).
241− runCatching {
243+ submitFlush(
242− flushExecutor.execute {
244+ {
243 runCatching {
245 runCatching {
244 prefs.edit().apply {
246 prefs.edit().apply {
245 if (text.isBlank()) remove(sessionId) else putString(sessionId, text)
247 if (text.isBlank()) remove(sessionId) else putString(sessionId, text)
246 }.apply()
248 }.apply()
247 }.onFailure { Log.w("DraftStore", "Failed to flush draft for $sessionId", it) }
249 }.onFailure { Log.w("DraftStore", "Failed to flush draft for $sessionId", it) }
251+ "flushDraft($sessionId)",
255+ /** Submit a flush task to the single-thread executor, surfacing a post-shutdown submit as a
256+ * logged warning instead of silently swallowing the RejectedExecutionException. Previously
257+ * the bare `runCatching { flushExecutor.execute { ... } }` swallowed RejectedExecutionException
258+ * after [shutdown], so the in-memory _drafts was already updated but the disk write never
259+ * landed with no signal — a draft silently lost. Logging makes the failure observable so
260+ * ordering bugs (shutdown before all VM onCleared flushes) are diagnosable. */
261+ private fun submitFlush(task: () -> Unit, label: String) {
262+ if (flushExecutor.isShutdown) {
263+ Log.w("DraftStore", "Dropping flush '$label': executor is shut down")
266+ runCatching { flushExecutor.execute { task() } }
267+ .onFailure { Log.w("DraftStore", "Failed to submit flush '$label'", it) }
252 private companion object {
270 private companion object {