M1-10: assemble the transcript at stop, and after a crash #26
No reviewers
Labels
No labels
area/agent
area/capture
area/editor
area/geo
area/security
area/sync
area/transcription
area/ui
area/vault
area/voice-memos
blocked
milestone/M1
milestone/M2
milestone/M3
milestone/M4
needs-decision
needs-hardware
p0
p1
p2
type/bug
type/enhancement
type/feature
type/idea
type/infrastructure
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Stash/stash-ios!26
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "m1-10-finalization"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #10, closes #22.
VTT.assemblelanded in the #9 follow-up, tested, and nothing called it. Every capture wasarriving as half the pair the vault consumes —
<base>_audio.m4aplus a<base>.parts/folder,never
<base>_transcription.vtt— and a recording the app was killed during never reached the codethat would have finished it.
Audio needed nothing here. Finalisation landed in #5, and a killed recording is already playable
because of the fragment interval.
What it does
Assembly reads the parts back off disk, not the cues still held in memory. Crash recovery has
no process state, so a memory path would be a second implementation that only ever runs after a
crash — the least-exercised code at the moment it matters most. One path, taken at stop and again by
the launch sweep.
The parts folder is deleted once the transcript is confirmed on disk and the part numbers run
1..n 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 — those words are worth having — and then
kept, with the missing numbers named. The folder is the only thing on disk that says the transcript
is short.
Launch finishes what a crash interrupted, once per process rather than from a view task, and
skipping whatever the recorder is currently holding.
Nothing can start while the last recording is still finalising. Pre-existing bug, and this
change lengthened the window:
stop()clearedisRecordingbefore awaiting the file, so a fastsecond tap replaced the sink and the older stop then tore down the newer recording.
Validated on device
wes-iphone, iPhone 17 Pro, iOS 26.Normal stop — a capture stops as exactly two files with no
.partsfolder, and the.vttround-trips through FFmpeg to SRT against its audio. This is also the only check that
RecordingTranscription.end()still reaches the assembler at all; no unit test can get there,because
begin()needs the speech engine.Crash recovery, unplanned and then confirmed by the breadcrumbs. A recording was force-quit from
the app switcher without stopping:
No stop was ever recorded for it, and the sweep assembled it one second after relaunch. Its audio
decodes clean end to end at 210.0 s despite never being finalised; the transcript has 34 cues from
0.0 s to 193.7 s with no gap over five seconds between them. The 16-second tail is the analyzer's
unfinalised audio, lost with the process — that is #25.
Six older captures left with
.partsfolders from #9 were finished by the sweep on install.Forty files came off the phone and zero
.partsfolders remain. Every transcript validates:exactly one
WEBVTT, cue numbers 1..n unbroken, starts monotonic, every end after its own start,no seconds field outside
0...59, audio present beside each.…182011-f0xwmf…182026-0n4n3o…192914-t722uw…085215-hg1eb0…112239-6zba2t…140926-kednow…113800-9qqy4gThe 44.6-minute one is the one worth noting: transcript and audio agree to the tenth of a minute
across roughly forty-five parts.
make test— 70 tests, 5 suites.make lint— 0 violations.Not validated
Stated plainly, because a claim of coverage that does not exist is worse than no claim.
during a recording; and starting a new recording while the previous stop is still saving. That
last one is the only check of the finalising guard, and nothing else covers it.
end()calls the assembler, thatStashAppcalls the launch sweep, and the finalising guard. All three need the speech engine or alive recording, and
AGENTS.mdholdsStashTeststo logic only. The first two are covered by thedevice results above; the third is not covered by anything.
TranscriptRecovery.run()called twice concurrently. Verified by two simulator relaunches,not by a test — a test of it would couple to the app's real documents directory and
AudioRecorder.shared.The Codex passes
Four rounds, all four artifacts in
.agent/m1-10-finalization/. The test pass earned its place thistime: it caught a process abort, not a weak assertion. My fix for a code-review finding used
[.atomic, .withoutOverwriting], which is afatalErrorinside Foundation rather than an error —it would have killed the app on every stop where somebody spoke, and the simulator pass had already
happened before that line existed. Publishing is a temp write plus
link(2)now.Writing those tests also turned up that one unreadable part was aborting the whole assembly, costing
the other thirty-nine.
Every disposition, including the rejections and why, is in
.agent/m1-10-finalization/SUMMARY.md.One change outside the issue, worth your call
.swiftlint.ymlnow setsfile_length: ignore_comment_only_lines: true.AudioRecorder.swiftwas at 396 lines of a 400-line limit before this branch, so the finalising fixcould not be added without breaking lint. The file is now 415 lines of which 177 are code — the
other 238 are why the microphone-swap and background-kill failures are handled the way they are.
Counting documentation toward a code-length limit pushes against what the build standard asks for,
and the pressure it creates is to delete the explanation rather than split the file.
The alternative was extracting the route-repair machinery into its own file, which needs a dozen of
the recorder's private members opened up to the module — a refactor of #6 and #7 code inside an
issue about finalisation. Flagged rather than done quietly, and easy to reverse if you would rather
take the split.
Follow-up filed
#25 — keep the unfinalised transcript tail, so a crash costs seconds rather than the last thing
said. It is written so it does not reverse the volatile-results decision in
AGENTS.md, and says toclose it if that separation cannot be held cleanly.
🤖 Generated with Claude Code