Transcription checkpoints and recovery
- Extension: transcribe closed chunks during capture
- Intended behaviour
- Pre-implementation audit
- Persistence model
- Commit boundaries
- Recovery and foreground execution
- Failure-case review
- Upgrade without rewriting existing history
- Implementation sequence and acceptance
- Source map for implementation
- Validation and device acceptance, 2026-11-04
Status: implemented 2026-11-04 following the 2026-11-03 design review. Validation results are recorded at the end of this page.
Extension: transcribe closed chunks during capture
Approved 2026-11-04. Extend the same checkpoint job to accept finalized chunks while its recording is still open. This is one inference per saved chunk, not repeated inference over a moving audio buffer or live word-by-word captions.
-
After
WavFileWriter.finish()closes a chunk, publish its exclusive index to the service worker and request a queue drain. This signal never waits for Whisper, model hashing, or database writes on the microphone loop. The worker reads only this closed prefix; the current writable WAV is excluded. -
One serialized Whisper worker first drains older queued recordings, then the closed chunks of the active capture. Growing jobs retain CPU-selected threads, model/decoder identity, and the existing checkpoint/attempt guards. A caught-up open job releases its attempt and yields instead of holding a worker/wake lock while waiting. Later chunk-close signals request another drain. A signal that arrives during a drain must still be serviced; there is no polling loop. Stop’s finalization holds service ownership until the durable marker and next queue request exist, even if the inference worker becomes idle concurrently.
-
Add a nullable exclusive
endChunkIndexto the job. Null means capture is open, regardless of how many chunks have transcribed. Store the final capture end on the recording too: Stop can precede creation of the first job. Stop closes the partial tail and atomically seals the job/recording and queues remaining work. Duplicate sealing must agree. Appended microphone captures clear the previous recording end and use a new job/range, retaining the original transcript. -
Append only contiguous chunk identities; never modify a registered identity or result. On later drains validate the existing registered prefix against saved audio and register newly closed chunks. A sealed range cannot grow. Publication requires a non-null end, the exact complete range, and successful results for all its chunks. Empty silence results count as success.
-
Recording remains
Recordingwhile the microphone is owned, even while its chunks transcribe. Whisper/model/storage failure pauses live transcription, preserves its results/audio, and leaves capture running. Stop exposes Failed and Retry; new chunks do not repeatedly retry a failed live job. -
Raw transcript publication, automatic polish, session merge, title generation, and completion events remain after capture stops and all chunks succeed. Partial checkpoint text stays internal. Automatic polish keeps the existing Initial-only policy and recorded profile/plan.
-
Process death while recording does not seal its range. On reopening, leave the recording in Recording for the existing explicit Stop and transcribe recovery action. That action repairs only the unregistered tail; registered closed sources must pass identity validation without being rewritten. If Stop was already persisted, normal startup recovery drains through the saved end.
Concurrency tests must cover catching up before Stop (no publication/polish), new chunks arriving during inference and at worker exit, Stop during inference, Stop before job creation, an unregistered final tail, live failure followed by continued capture and explicit Retry, process reopen, append generations, duplicate/conflicting sealing, and migration of existing frozen jobs. Physical device acceptance must check continuous audio, screen-off progress, and sustained heat/speed. Smaller post-Stop wait is expected, but real-time completion is not promised when inference is slower than capture. Existing source audio remains the recovery authority; no new transcription path repairs or deletes it.
Intended behaviour
Save each successfully transcribed WAV chunk before starting the next one. When interrupted work resumes, load those results and start at the first unfinished chunk. If chunks 0 through 11 have committed and the process dies while transcribing chunk 12, recovery starts at chunk 12. No completed chunk needs Whisper again, and no saved text is appended twice.
Recovery also needs to know whether Whisper has finished, whether automatic polish has finished, and whether the recording has been incorporated into its session. A chunk counter alone cannot protect those later steps.
Keep the existing worker pool. On the Motorola Edge 50 Fusion the current CPU selector chooses four threads: four cores report 1,958,400 kHz and four report 2,400,000 kHz; the selector counts the faster group. The code currently detects this count rather than fixing it globally. Persist the selected count in each job so retries use the same setting. This implementation does not change the CPU selection policy or the debug benchmark’s thread choices.
Review clarification: retain CPU-aware selection rather than a universal four-thread limit. The existing upstream heuristic has edge cases to assess separately: its minimum of two can oversubscribe a single available CPU, while equal-frequency cores can fall through to two threads even on a larger device. Any hardening should bound the result by available processors and handle homogeneous CPUs explicitly. More threads are not automatically faster; the current phone’s representative results support four workers, not a rule that every core must be used. No selector changes are included in this implementation.
Pre-implementation audit
| Previous path | Consequence |
|---|---|
|
Progress callbacks contain counts, not text. Process death loses all partial transcription work, while saved WAVs remain. |
|
It marks the start of newly appended audio, not the next unfinished transcription chunk. Reusing it as a progress cursor would mix two meanings. |
|
Filesystem enumeration is not a reliable capture order. The selected append range must be ordered and validated before any skipping. |
|
A crash after raw-text persistence can turn an initial retry into an append. A resumed recording can append the same range again. An initial version can also be missing or an active version stale. |
Interrupted |
A restart during polish can rerun Whisper and append text which was already saved. |
|
A crash in that gap can leave finalization unfinished without a queued item to repair it. Session append itself has no transcription-job completion guard. |
JNI logs a nonzero |
Kotlin can consume segments despite inference failure. That must be fixed before declaring a chunk checkpoint successful. |
JNI explicitly sets |
The existing decoder does not require previous chunks' text context to continue. A fresh native model context can process the next chunk; model/decoder compatibility still needs checking. |
|
Shutdown and a replacement worker need explicit ownership/cancellation guards, so a late old result cannot modify a resumed job. |
Failure labels infer the failed stage from |
A failed append can be labelled as a polish failure or lack the needed retry control. Recovery must expose its actual failed stage even when older text exists. |
These are source-derived failure windows, not evidence that every one occurred in the user’s historical runs. Source entry points are listed below.
Persistence model
Use the existing SQLDelight/SQLite database for both results and work state. Avoid a second checkpoint file plus a database counter: those would require a separate protocol to keep two stores consistent.
The additive migration creates two tables and records active-job/capture intent on a recording:
| Entity | Contents |
|---|---|
|
Job ID, recording ID, Initial/Append kind, frozen start/end chunk indices, stage, current attempt ID, model fingerprint, decoder signature, selected thread count, base raw text, base recording version ID, committed raw version ID, automatic-polish profile/plan/state, raw base version for polish, finalization result, recovery count, and known failed inference time. Recording failure diagnostics remain on the recording row. |
|
Job ID plus absolute capture chunk index as a unique key; canonical filename, byte count, sample count, SHA-256, nullable result text, committed processing duration, and completion timestamp. Manifest rows exist before inference; result text is null until committed. |
|
Identifies the one unfinished generation owned by this recording. Claim it in the same transaction that creates the job and its manifest. Completed jobs remain separate from any later microphone-resume job. The pending Initial/Append intent is stored before processing so a crash before job creation is unambiguous for new captures. |
Empty result text is a valid successful checkpoint for silence. Null means unfinished. Do not derive completion from nonblank text, a progress callback, or the presence of a WAV alone. A unique job/chunk key prevents double insertion; an identical duplicate commit is harmless, while a conflicting result is an error rather than an overwrite. Derive progress and the next unfinished chunk from the manifest/results; an independently maintained cursor is unnecessary.
For completed captures, freeze and validate the ordered manifest after capture is finalized. The extension above registers a growing closed prefix before sealing it. Parse numeric indices from canonical chunk filenames, reject gaps/duplicates and invalid WAVs, and select the append range by those indices. Validate the expected PCM format and whole samples. A valid finalized zero-sample tail can commit an empty result without invoking Whisper; an incomplete/invalid header is not equivalent to that valid empty WAV. No silent skipping of damaged files. Compute identity from the same bytes decoded for inference. Source files are immutable during a job; recheck their identities on recovery, including files whose results were already saved. Unexpected additions or changes pause the job for diagnosis rather than extending its manifest or deleting checkpoints.
Identify the actual model contents and the decoder contract (vendored revision, language, decoding parameters, audio format, text-joining rules). A filename or the app version alone is insufficient. Reuse a verified download fingerprint if available, otherwise hash the model when establishing/validating identity; do not hash the whole model for every chunk. A mismatch pauses recovery and preserves all results. Explicitly starting a different transcription generation can be designed later without erasing the previous one.
The job’s start index remains distinct from its saved-result frontier. A later
microphone resume creates a new Append job for new chunks and retains the
earlier job and transcript versions. Do not infer job kind from rawText after
the job has started.
Commit boundaries
The durable stages are:
Transcribing -> TranscriptCommitted -> PostProcessing -> Complete
Failure or a conflict can pause any unfinished stage without moving it back to the beginning. Recording status is a UI/queue projection; the job determines which operation needs to run. Each database transition checks the expected job ID, attempt ID, stage, recording existence, and relevant version IDs inside a short transaction. Model inference, filesystem reads, and suspend calls stay outside that transaction. A process-local mutex alone is not a durable completion guard. Each new worker ownership gets an attempt ID, so a delayed result from a stopped service instance cannot commit over its successor. Duplicate requests to a live owner do not replace that token. Checkpoint completion guards and explicit deletion queries must work even if the driver has not enabled foreign keys; verify the driver’s configuration rather than assuming declared FKs enforce it.
-
Create/claim: store the manifest, base text/version, decoder identity, and automatic-polish choice/profile. Do not create a second active job on duplicate start/Retry requests. Changes to the automatic-polish setting apply to later jobs; a retry retains this job’s recorded plan.
-
Checkpoint one chunk: propagate native errors to Kotlin, then commit its text and timing. Only after commit succeeds may progress advance and the next chunk begin. A crash after inference but before commit repeats that one chunk. If the commit succeeded but its acknowledgement was lost, recovery finds the row and skips it. Validate the complete contiguous range before publishing.
-
Publish the transcript: in one transaction, assemble saved chunk text in order, update raw text/diagnostics, create the one InitialTranscription or ResumeAppend version, update its active pointer, and advance the job stage. Initial text is assembled from the job’s complete range. Append raw text is derived from the saved base raw text plus this job’s delta; the active version appends the delta to the latest edited/polished active version read in that transaction, provided source raw text and membership still match. Its actual base version ID records that choice. Never concatenate blindly onto whichever raw text happens to be present during a retry.
-
Optional automatic polish: only Initial jobs qualify, matching current behaviour. Track Pending/Running/Succeeded/Failed/Skipped/Superseded separately. Store the base version before inference. Commit a successful polish version, active pointer, and job state together. A crash before that commit reruns only polish; a crash after it does not create another polish version. Ordinary polish failure leaves raw text available and proceeds to finalization with the existing polish-failure diagnostic. Preserve intervening manual edits: a stale polish result is superseded rather than replacing the user’s version.
-
Finalize: atomically apply the pending session choice or session merge, store membership/provenance and completion markers, consume the choice, mark the recording Ready, mark the job Complete, and release its active-job pointer. Read the current session version inside this transaction to preserve session edits and existing append/out-of-date behaviour. A repeated finalization reads the completed result and creates neither another session nor another merge. If the chosen session was deleted, finish standalone without losing its text.
Edits to the existing prefix are retained when publishing an Append job. If source raw text or recording membership changed incompatibly before publication, preserve the checkpoint results and current versions and pause for review. Do not overwrite a newer user version to satisfy an outdated job. Serialize membership changes, deletion, and finalization through transactional repository operations with these guards. Explicit recording/session deletion removes its checkpoint rows as part of the same operation; a late inference result cannot recreate a deleted recording or its job.
Title generation stays with its existing durable queue after transcript finalization. Completion notifications are best-effort signals after commits; they do not determine recovery state. Chunk text stays in checkpoint rows until the complete range is published, so title generation and ordinary transcript editing never mistake a partial prefix for the finished raw transcript.
Recovery and foreground execution
On the next app launch, reuse the existing foreground queue to resume an unfinished job from its recorded stage. Explicit Retry on a failed job continues that job and keeps its results. Duplicate startup/Retry requests converge on the same job. Jobs already Complete and existing Ready recordings are not replayed.
First scope: resume on app launch or explicit Retry. This does not promise that Android will restart the process immediately after an OEM kill or reboot. Force stop requires user interaction to leave the stopped state; checkpointing preserves work for that later opportunity. See the Android stopped-state documentation.
Keep one FIFO Whisper worker and the existing foreground notification. Reuse the scoped wake-lock approach for production inference, with release on every exit and a documented timeout/renewal policy suitable for long recordings. Restore progress from committed rows, for example 12/47, rather than resetting to zero. Show stage-specific failure and Retry even when an earlier transcript already exists. Keep microphone "resume recording" distinct from transcription Retry. The debug benchmark continues to read WAVs without publishing results into production jobs or transcript versions.
Handle service shutdown/timeout as interruption, preserving committed results.
Android places time limits on background dataSync foreground services; a
foreground notification does not make a job uninterruptible. A timeout handler
must stop promptly and cannot wait indefinitely for native inference to finish.
Invalidate the old attempt, cancel its orchestration scope, and reject late
commits. Serialize native release after any in-flight call; never free the
model context while JNI is executing. A timed-out bound service creates a fresh
orchestration scope when started again; cancellation completion handlers release
queue/title ownership even for coroutines that never started. JNI is not forcibly
aborted mid-call: a surviving in-flight call can finish, but its invalidated
attempt cannot commit. A replacement on that same service waits for serialized
inference to return. Process death ends the call; the unfinished chunk is repeated.
See foreground service timeouts.
Crash-loop guard: allow one automatic recovery at an unchanged chunk/stage boundary, then require explicit Retry if another process death occurs without a durable advance. Persist the count and reset it on a committed advance; duplicate requests within one live owner are not recovery attempts. Explicit Retry resets this guard without removing saved chunk results.
For diagnostics, persist monotonic durations for committed chunks and separately record known failed-attempt durations. Do not count overnight queue/suspension time as transcription CPU work or claim an exact total for a process that died before recording its final timing. Log job/chunk/stage identifiers and outcomes, not recognized speech.
Failure-case review
| Failure point | Required recovery/result |
|---|---|
Model load or inference fails before a chunk commits |
Keep earlier results; Retry starts at the same unfinished chunk. JNI failure is an error, not successful empty output. |
Successful silence/empty output |
Commit an empty string and skip it on recovery. A valid zero-sample tail needs no inference call. |
Death during inference or just before chunk commit |
Repeat only that uncommitted chunk using a fresh native context. |
Death during or just after chunk commit |
Transaction is absent or complete. Reload it before deciding whether to repeat inference. |
Storage full or checkpoint write fails |
Do not advance progress or begin another chunk. Preserve audio and prior commits; retry once storage is available. Diagnostics themselves may be unwritable. |
All chunks committed; death before transcript publication |
Assemble persisted results and publish without rerunning Whisper. |
Death inside transcript/version publication |
Raw text, version, active pointer, and job stage all roll back or commit together. No duplicate append or missing initial version. |
Death during automatic polish or its commit |
Retain raw output; rerun only uncommitted polish, or reuse its committed result. |
Death during session creation/merge or Ready transition |
Membership, merge, pending choice, Ready, and job completion roll back or commit together. No duplicate session or append. |
Edit or membership change while work is running |
Validate expected versions/membership at commit. Preserve user edits; supersede stale polish, and pause incompatible publication. |
Duplicate start/Retry or repeated startup recovery |
One active job and worker; repeat commits/finalization have no additional effect. |
Late callback from a stopped/replaced worker |
The old attempt ID fails its commit guard and cannot change the new owner’s job or failure state. |
User deletes the target recording/session |
Respect the explicit deletion; reject late commits. A deleted pending destination leaves a surviving recording standalone. |
Missing, changed, malformed, or unexpectedly added audio |
Preserve files and checkpoint results; report the mismatch. Never renumber, auto-repair, truncate, or silently skip source audio during transcription recovery. |
Different model/decoder or incompatible app update |
Preserve results and pause rather than mixing incompatible output. A compatible app update can resume the same saved contract. |
Kill/reboot/force stop or service timeout |
Already committed data survives the process; recovery runs when the app is next allowed to execute. No reliance on finally/onDestroy for correctness. |
Resumed capture adds another audio range |
New job and frozen range; prior raw text and active edits retained; append exactly once. |
SQLite transactions provide the all-or-nothing database boundary described in SQLite’s atomic commit documentation. Validate journaling/synchronous settings on the actual app driver before making power-loss durability claims. This protects committed progress; it cannot recover speech never flushed to storage or repair a physically failed storage device. Audio-file repair after interrupted microphone capture remains a separate, explicitly invoked operation, outside transcription recovery.
Upgrade without rewriting existing history
The during-capture extension adds migration 11.sqm (schema version 12): nullable
recordingEntity.captureEndChunkIndex and transcriptionJobEntity.endChunkIndex.
Version-11 jobs came from finalized manifests, so their end is backfilled from
their last registered chunk plus one. Preserve their identities, results, stages,
attempts, text, and history. New open jobs have no end until Stop. Existing Ready
recordings receive only a null column; no transcript/audio/history is rewritten.
The previous generated SQLDelight schema was version 10 (migrations through
9.sqm). The additive 10.sqm migration produces version 11. Add tables/reference fields without deleting or rewriting existing audio,
raw text, versions, or session membership. Use normal upgrade migrations;
no uninstall, database reset, or destructive fallback.
The user confirmed during review that there are currently no unfinished runs. There is therefore no known ambiguous legacy run to repair for this device. The rules below remain defensive upgrade behaviour, not a required cleanup of the user’s current recordings.
-
Existing Ready recordings remain untouched, with no invented per-chunk text.
-
Legacy unfinished rows with no raw text, versions, or conflicting append state can safely create a new Initial job from their validated saved audio.
-
Legacy unfinished rows with raw text or existing versions are ambiguous: old code did not store whether an append/polish/merge already committed. Preserve them and expose recovery review instead of guessing and auto-appending. A future explicit rebuild must produce a new candidate/version and preserve the legacy raw text and active edits; it must not overwrite them in place.
-
An interrupted microphone capture remains Recording until the existing explicit stop-and-transcribe action finalizes it. This migration does not silently modify its WAVs or reinterpret capture as a completed job.
Implementation sequence and acceptance
-
Add the schema, guarded repository operations, and a shared orchestration layer with injectable chunk inference. Validate upgrades from version 10 and real file-backed SQLite close/reopen, not only an in-memory repository.
-
Propagate JNI failures, validate stable manifests, and connect per-chunk inference/checkpointing. Implement atomic transcript/version publication and post-processing/finalization before enabling the new path. A chunk-only change must not ship while the later duplicate-append windows remain.
-
Wire existing queue recovery/Retry and stage-aware progress/failures; add production lifecycle handling. Verify process interruption, screen-off execution, failure cleanup, and data preservation on Android.
Tests must interrupt before/after each durable boundary and reconstruct the coordinator/repository from disk. Assert which chunk indices inference actually receives, exact final text, unchanged original/edited prefixes, version counts and IDs, session membership, and final job stage. Cover initial and multiple appended ranges, empty successful text, injected native/storage failures, duplicate requests, stale worker attempts/edits, deleted targets, and incompatible inputs. Inject an SQL failure during publication/finalization to prove rollback of all effects. A killed process test must demonstrate committed chunks are skipped; a progress-count assertion alone is insufficient. Use deterministic inference substitutes for exact assembly assertions; device tests must establish saved results are reused rather than requiring a fresh native process to reproduce every previously generated character.
Run destructive interruption tests only on a disposable emulator/test install containing synthetic fixtures. Before an in-place device update, back up and verify audio/database data; afterwards compare saved files and existing recording/session/version contents. Do not interrupt or fault-inject the user’s important recordings. Performance/energy costs of hashing and database commits need a representative recording check, separate from correctness acceptance.
Source map for implementation
-
shared/src/androidMain/kotlin/dev/sysout/inmyhead/capture/RecordingService.kt:transcribeRecording,processQueuedRecordings,retryTranscription, and capture finalization. -
shared/src/androidMain/kotlin/dev/sysout/inmyhead/transcribe/WhisperTranscriber.kt: chunk inference, ordering, and callback boundary;WaveDecoder.ktfor validation. -
whisperNative/src/main/cpp/jni/jni.c:fullTranscribe, native error propagation, and the current independent-chunk decoder settings. -
shared/src/commonMain/kotlin/dev/sysout/inmyhead/data/TranscriptRepository.kt: raw/append writes, recording versions, automatic polish, pending session choice, session merges, and deletion. -
shared/src/commonMain/sqldelight/dev/sysout/inmyhead/database/InMyHeadDatabase.sqandmigrations/: job/checkpoint schema and atomic transition queries. -
AndroidAudioCaptureController.ktplus transcript/list/activity UI: startup recovery, Retry, restored progress, and failure-stage presentation.
Validation and device acceptance, 2026-11-04
During-capture extension
-
All 38 JVM tests passed, including eight new cases for growing jobs, Stop racing with inference or job creation, late tail registration, live failure, open-capture process reopen, atomic/idempotent sealing, appended ranges, and preservation of version-11 frozen jobs/results. Existing recovery tests remain passing. The publication transaction also rejects a fully transcribed open job.
-
Debug APK and instrumentation APK builds passed. Lint reported zero errors and 31 warnings. Instrumentation includes a new closed-prefix/WAV-tail recovery test; Android runtime tests for this extension have not yet been executed.
-
Migration 11 to 12 was applied to an isolated clone of the previously verified phone database backup. All existing columns/values in 14 recordings, one session, and 56 versions were unchanged; SQLite integrity remained
ok. This is offline migration evidence, not validation of the current live device. -
The packaged DEX was checked for the new capture end marker, prefix reader, sealing operation, and stopped-capture notification guard. No app-owned OpenMP runtime was reintroduced. Current local evidence is retained in the ignored
tmp/live-chunk-implementation-20261104/directory. -
On the subsequent install request, wireless ADB rediscovered the phone. Waited for an existing recording’s polish/title work to finish, verified a full Debug backup, installed in place, and launched successfully. The installed APK hash matched the verified local build and the live database upgraded to version 12. The user explicitly confirmed deleting recordings during verification; those deletions were retained. All old fields in the nine remaining recordings, one session, and 40 versions were unchanged, as were all 67 remaining audio files and existing preferences. SQLite integrity was
ok; backups and evidence are retained intmp/live-chunk-install-20261104/. No recording was created, retranscribed, interrupted, or restored by the agent. Physical capture, audio continuity, thermal/screen-off behaviour, and open-capture interruption remain user acceptance items.
User follow-up confirmed that force-kill/reopen recovery and immediate closed-chunk processing both worked on the phone. A presentation mismatch remained: Stop could leave the recording Queued while its existing worker and notification showed saved progress. The UI follow-up uses that live progress for its Transcribing label, rotating mark, and chunk bars, retaining the durable queue state. Numeric counts remain in the notification only, following the user’s clarification. See Activity feedback UX.
For acceptance, update In My Head (Debug) in place and create a new disposable 90-second recording. Speak identifiable phrases across the 25-second boundaries, keep speaking while Whisper runs, and stop. Confirm complete audio playback, ordered transcript text without gaps/repeats, and polish only after Stop plus all transcription. A second disposable capture can be force-stopped before Stop; on reopen use the explicit Stop and transcribe action and check saved results are reused. A post-Stop interruption should resume automatically on reopening. Check short captures under 25 seconds and microphone append to an edited standalone recording too. The user subsequently confirmed interruption recovery and during-capture processing. Version 0.4 packaging and remaining CI prerequisites are recorded in Release delivery.
Earlier fixed-manifest implementation
-
Debug app/test APK builds passed, including all three native ABIs. All 30 JVM tests passed (16 recovery/migration cases plus 14 existing tests). Lint passed with zero errors and 31 warnings.
-
Six quick Android checks passed: Android-driver version-10 upgrade and atomic rollback, WAV identity/format validation, worker-pool identity, missing-audio cleanup, and restarting a bound service after timeout.
-
A real two-phase test on a disposable, read-only Android 15 x86_64 emulator used the actual
small.enmodel and three synthetic WAV chunks. The host force-stopped the debug app after the first chunk committed. A new process resumed the same job at 1/3 and inferred chunks 1 and 2 only. Chunk 0’s saved text/timing were unchanged and exactly one initial transcript version existed. The fixture’s rows and audio were deleted afterwards. The test is correctness evidence, not a useful speed or thermal benchmark. -
The Motorola Edge 50 Fusion reconnected automatically and the verified debug APK was installed in place. The installed APK hash matched the final artifact. The real version-10 database upgraded to 11 with all 75 WAV files and every existing field in 14 recording rows, one session row, and 56 transcript-version rows unchanged. SQLite integrity was
ok; no jobs were created for completed recordings. Full before/after backups were retained. No user recording was retranscribed or interrupted, and the production app was not updated. The user’s new-recording acceptance test remains pending.
Local evidence is in tmp/checkpoint-implementation-20261104/, including the
final build/test reports, native call trace, and a source snapshot taken before
implementation. These artifacts are ignored by Git. Later release preparation is
recorded in Release delivery.
For device acceptance, update In My Head (Debug) in place. Let important work finish first, then use a new disposable recording of about 90 seconds. After stopping capture, background/lock the phone and confirm the foreground notification advances its saved-chunk count. Before completion, force-stop only the debug app and reopen it. It should resume the saved count and finish without another recording or repeated transcript section. On a second disposable capture with automatic polish enabled, interruption during Polishing should resume that stage without another Whisper transcription. Do not uninstall or clear app data.