M1-10: Finalisation and crash recovery #10

Open
opened 2026-08-08 20:03:45 -04:00 by agent · 0 comments
Member

Turn a stopped recording into the finished pair on disk, and finish the job after a crash.

What this exists to prevent

On iOS this was found late: VTT.assemble had landed, was tested, and nothing called it. Every
capture was arriving as half a pair — audio and a parts folder, with no transcript — and a recording
the app was killed during never reached the code that would have finished it. Assume the same gap
here and wire it deliberately.

Scope

  • One assembler, reading from disk. It takes the parts folder, reads the parts back off disk,
    joins them and writes the transcript beside the audio. Reading from disk rather than from
    whatever is still in memory is the point: crash recovery has no process state, so a memory path
    would be a second implementation that only ever runs after a crash — which is to say, the path
    that is never exercised is the one that has to work.
  • Parts are ordered by their parsed number, and only names the writer would have produced are
    read at all. An atomic write leaves a temporary file in the same folder, and a looser match
    splices half a part into the middle of somebody's words.
  • The folder goes once the transcript is confirmed and the parts are unbroken. A part that never
    landed leaves a transcript that reads as whole while missing a minute of speech — so a sparse
    folder is assembled anyway, because those words are worth having, and then kept, with the
    missing numbers named. The folder staying put is the only thing on disk that says the transcript
    is short.
  • Launch finishes what a crash interrupted. Any capture with parts and no transcript is
    assembled, once per process, skipping whatever the recorder is currently holding.
  • Nothing can start while the last recording is still finalising, and the message saying so
    clears itself.

If M1-05 chose the remux path

Then audio recovery lives here too: an orphaned capture file with no finished container beside it
gets finished on the same launch sweep. If M1-05 chose raw ADTS, audio needs nothing and this issue
is transcripts only.

Android's process death is not iOS's

Doze, background restrictions and the OEM's own killer all end a process differently from an iOS
watchdog, and a foreground service changes when it happens rather than whether. The recovery path
must not assume it was reached by a normal launch. Worth deciding whether the sweep runs in
Application.onCreate, in the first activity, or in WorkManager — the iOS equivalent ran once per
process from the app entry point and skipped the active capture.

Done when

A stopped recording leaves exactly the finished pair with no leftover folder, and a recording
force-stopped mid-flight is assembled on the next launch with the audio intact.


Milestone 1 of Stash for Android, mirroring stash-ios M1. M1 is a functioning app on the test
device with no sync at all — record voice memos reliably and get them off the device by hand.
Sync arrives in M3.

Test device is a Pixel 6 (Tensor G1) borrowed from a friend, until something better turns up.
Stock Android, so no OEM battery-killer behaviour to fight — worth remembering that a phone from
another manufacturer may not be as forgiving about background work.

Translation of Stash/stash-ios#10. Where the platform forces a different answer this issue says so; where it
does not, the iOS issue is the fuller statement of intent and should be read alongside this one.

Working agreement for every issue in this repo: feature branch, tested before the PR opens
(on-device where the label says so), then Wes reviews the PR and we walk the code together. Docs
update in the same commit. A PR that takes more than 20 minutes to review is too big — say so and
split it.

Turn a stopped recording into the finished pair on disk, and finish the job after a crash. ## What this exists to prevent On iOS this was found late: `VTT.assemble` had landed, was tested, and **nothing called it**. Every capture was arriving as half a pair — audio and a parts folder, with no transcript — and a recording the app was killed during never reached the code that would have finished it. Assume the same gap here and wire it deliberately. ## Scope - **One assembler, reading from disk.** It takes the parts folder, reads the parts back off disk, joins them and writes the transcript beside the audio. Reading from disk rather than from whatever is still in memory is the point: crash recovery has no process state, so a memory path would be a second implementation that only ever runs after a crash — which is to say, the path that is never exercised is the one that has to work. - **Parts are ordered by their parsed number**, and only names the writer would have produced are read at all. An atomic write leaves a temporary file in the same folder, and a looser match splices half a part into the middle of somebody's words. - **The folder goes once the transcript is confirmed and the parts are unbroken.** A part that never landed leaves a transcript that reads as whole while missing a minute of speech — so a sparse folder is assembled anyway, because those words are worth having, and then **kept**, with the missing numbers named. The folder staying put is the only thing on disk that says the transcript is short. - **Launch finishes what a crash interrupted.** Any capture with parts and no transcript is assembled, once per process, skipping whatever the recorder is currently holding. - **Nothing can start while the last recording is still finalising**, and the message saying so clears itself. ## If M1-05 chose the remux path Then audio recovery lives here too: an orphaned capture file with no finished container beside it gets finished on the same launch sweep. If M1-05 chose raw ADTS, audio needs nothing and this issue is transcripts only. ## Android's process death is not iOS's Doze, background restrictions and the OEM's own killer all end a process differently from an iOS watchdog, and a foreground service changes when it happens rather than whether. The recovery path must not assume it was reached by a normal launch. Worth deciding whether the sweep runs in `Application.onCreate`, in the first activity, or in `WorkManager` — the iOS equivalent ran once per process from the app entry point and skipped the active capture. ## Done when A stopped recording leaves exactly the finished pair with no leftover folder, and a recording force-stopped mid-flight is assembled on the next launch with the audio intact. --- *Milestone 1 of Stash for Android, mirroring `stash-ios` M1. M1 is a functioning app on the test device with **no sync at all** — record voice memos reliably and get them off the device by hand. Sync arrives in M3.* *Test device is a **Pixel 6** (Tensor G1) borrowed from a friend, until something better turns up. Stock Android, so no OEM battery-killer behaviour to fight — worth remembering that a phone from another manufacturer may not be as forgiving about background work.* *Translation of `Stash/stash-ios#10`. Where the platform forces a different answer this issue says so; where it does not, the iOS issue is the fuller statement of intent and should be read alongside this one.* *Working agreement for every issue in this repo: feature branch, tested before the PR opens (on-device where the label says so), then Wes reviews the PR and we walk the code together. Docs update in the same commit. A PR that takes more than 20 minutes to review is too big — say so and split it.*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-android#10
No description provided.