Skip to content

feat(meeting): keep every recording on the phone for 90 days - #58

Merged
WingedGuardian merged 19 commits into
mainfrom
feat/meeting-mic-keep-90d
Oct 23, 2026
Merged

WingedGuardian merged 19 commits into
mainfrom
feat/meeting-mic-keep-90d

Conversation

@WingedGuardian

@WingedGuardian WingedGuardian commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

What changes

The phone keeps every recording for 90 days, even after the edge confirms it holds a copy. This replaces #57's delete-7-days-after-confirmation rule.

  • The sync pass never deletes. A week after confirmation it asks the edge once (/have) whether it still holds those exact bytes:
    • yes: it leaves an X.checked mark and keeps the phone copy;
    • no: the file goes back to the upload queue;
    • no answer: it asks again on a later pass.
  • Retention owns every delete (RetentionPolicy.sweep, run by LocalRecorder.purgeDir):
    • anything older than 90 days goes;
    • under storage pressure (under 1 GB allocatable), copies the edge confirmed may go early, re-confirmed ones first, oldest first. Deletion stops as soon as there is room again;
    • an unsynced recording is the only copy and is never deleted early. A full phone turns the local backup off at its floor instead. This removes the old 45-day low-storage floor.
  • X.synced.aac exists only after the edge returns a hash-verified receipt, so nothing deleted early was never held by the edge.
  • The purge's folder walk moved into a Context-free purgeDir(dir, now, hasRoom), so its rename race is tested.
  • The status line reads "kept 90 days unless storage runs low".
  • A re-check claims its recording under the folder lock, so a low-storage purge can never delete a copy the edge has just disowned (Codex round 1). Every sync call has a 10-minute overall timeout.
  • Version 0.6.1 (versionCode 7).

Tests

  • 97 unit tests pass.
  • New tests:
    • retention order, the stop once there is room, the only copy never deleted early, and 90/91-day boundaries;
    • purge on a real folder, including a held copy the edge disowns mid-purge;
    • the re-check is asked once, and its mark travels with migrations and deletes.
  • Verify-RED: each new rule's test fails when its line is mutated (10 mutants, all killed).

Shape: about 300 changed lines, one concern.

E2E: after the APK is installed, record a short meeting and confirm the segment becomes .synced.aac and stays on the phone. A week later, confirm an X.checked mark appears beside it and the copy is still present.

…nnect

- MicLevelMonitor: fail-closed Mic signal. Green only with a non-zero frame in
  the last 2 s; red after 5 s of all-zero audio or 5 s with no frames, and at
  once when Android reports the capture silenced (AudioRecordingConfiguration,
  API 29+). Re-judged on a 200 ms clock so a stalled capture loop cannot stay
  green.
- Live mic meter in the app and a level bar in the notification; the
  notification leads with the worst signal. One alert per incident: the mic
  alert clears only after 3 s of continuous sound.
- TailscaleLink: when no VPN is up, ask Tailscale to connect through its
  exported CONNECT_VPN receiver; after 20 s without a VPN, alert with a
  tap-to-open action. The network note shows only while delivery is failing.
- A sticky restart after an OS kill no longer tries to reopen the mic (Android
  14+ refuses background microphone FGS starts); it posts a notification that
  opens the app, which restarts capture through the normal Start path.
- Setup checklist (Always-on VPN, never-sleeping apps, mic access tile) and a
  banner when notifications are disabled.
- Version 0.4.0 (4) with the build time shown.
…e floor

- LocalRecorder: every capture is also written on the phone, independent of
  the live stream. The capture loop hands frames to a bounded queue (never
  blocks); a per-run writer thread encodes AAC-LC 32 kbps 16 kHz mono and
  writes raw ADTS frames in 5-minute segments, each with a sidecar written at
  open (UTC start, run id, sample position). Dropped frames are carried in
  order and encoded as silence, so positions track capture time.
- Retention: older than 90 days is deleted; below 1 GB allocatable, older than
  45 days too; never anything newer than 45 days. Runs at each capture start,
  under storage pressure, and on every app open. Open and future-dated
  segments are never touched.
- Storage floor: below 300 MB the backup pauses (resumes above 400 MB) and the
  live stream continues. Encoder failures retry after 30 s, permanent after 3.
- Fail-closed Local backup signal (green only while bytes reach disk) with a
  loud alert when it goes off; each closed file is re-read and its valid audio
  length shown; an "On this phone" summary reads the folder directly.
- Version 0.5.0 (5).
- SegmentSync: the recordings folder is the queue (X.aac waiting,
  X.synced.aac confirmed). Each closed segment, including the short final one
  at Stop, is uploaded; it becomes synced only when the edge's re-read sha256
  and length equal the phone's own. After a 7-day grace (sooner only when
  storage is low) the phone asks the edge again (/have, re-hashed from disk):
  yes deletes, no re-queues, no answer keeps. Any doubt keeps the file.
- Segment names carry a per-run UUID (the in-memory run counter restarts with
  the process and could collide); older names are migrated once.
- SyncWorker (WorkManager): a kick on every closed file and app open
  (REPLACE, so a recovered network is used at once) plus an hourly catch-up
  that survives restarts and reboots; one pass at a time. A file the edge
  cannot take now does not block the others. A loud alert if anything has
  waited over a day; the status shows waiting / confirmed / refused /
  expired-before-sync.
- Retention purge shares a folder lock with sync and reports unsynced
  expiries. Version 0.6.0 (6).
…s as the tailnet

- Tap-to-resume restores the persisted capture settings (endpoint, token,
  model) instead of the build defaults a fresh screen shows; with none saved it
  refuses to start rather than silently using defaults.
- The tailnet check requires a VPN network carrying a Tailscale address (the
  RFC 6598 /10 or Tailscale's IPv6 /48), so another VPN no longer suppresses
  auto-connect and the Tailscale alert.
- The meter drops to zero once sound is no longer fresh.
- Stop clears the network note before publishing the stopped state.
- The notifications-off banner also catches the app-level switch and a muted
  failure channel.
…into feat/meeting-mic-local-backup

# Conflicts:
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/MainActivity.kt
…eview

- Each queued frame carries its capture time; a segment's UTC start is when
  its first audio was captured, not when the lagging writer encoded it.
- Filling dropped audio with silence stops as soon as the writer goes off.
- The writer holds its own wake lock in renewed short leases for its whole
  run, so the drain after Stop stays awake however long the meeting was.
- A write error resumes once free space is back above the 300 MB floor; only
  a full disk waits for the 400 MB margin.
- An encoder that fails to configure is released; a missing encoder counts
  as an encoder failure, not a storage failure.
- The storage summary is refreshed again after the final file has closed.
…l-backup

# Conflicts:
#	clients/android-mic/app/build.gradle.kts
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/MainActivity.kt
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/MicStreamService.kt
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/ProblemAlerts.kt
#	clients/android-mic/app/src/main/res/layout/activity_main.xml
#	clients/android-mic/app/src/main/res/values/strings.xml
…saved

- Audio dropped after the last queued frame is filled with silence at Stop,
  so the file is as long as the capture.
- An encoder that does not finish a segment (no end-of-stream within the
  bound, or no input slot for it) is an encoder failure, not a finished file.
- A failed fsync turns the backup off with an alert instead of being ignored;
  a failed close reports the last file as not fully saved.
- minSdk 30: from Android 11 other apps cannot read this app's external
  files folder, where recordings are kept.
… encrypted only

Merges the updated local-backup base, then:
- The sync line refreshes whenever a sync job changes state, so it is
  current while the screen stays open.
- Legacy segment names migrate to a name derived from the old one, so an
  interrupted migration finishes next time and the sidecar follows its
  audio; retention runs the migration before sweeping orphan sidecars.
- Sync uses only a wss:// (https) address; with a plain ws:// address the
  files wait on the phone and the status says why.
- The upgrade fallback saves the last capture's settings as the sync
  endpoint, not the form's build defaults.
…e, never overwrites

- The local writer is finished when the capture job completes, so the read
  in flight at Stop is kept; a job cancelled before it started still
  finishes its writer.
- Each new writer waits for the previous one in this process to release
  its encoder, across service restarts.
- A segment is never opened over an existing recording.
- A segment that loses its tail at a 5-minute rollover is reported and
  recording continues with a fresh encoder; only repeated failures stop it.
- Retention removes a sidecar only once its audio is really gone.
…nto feat/meeting-mic-sync

# Conflicts:
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/LocalRecorder.kt
Each capture run registers its endpoint, and every segment records it in a
small X.dest file when it opens. Uploads and the later "still held" checks
go only to that endpoint, so changing the bridge or token no longer
redirects queued recordings. A recording captured over ws:// has no secure
destination: it stays on the phone and is reported.

Recordings from earlier builds take the endpoint those builds were using,
once, and only if they were made before the upgrade. One unreachable bridge
no longer holds back uploads to another.

The stuck-recording alert now also runs from a job with no network
requirement, so a phone offline for days still gets it, and recordings
that can never sync raise it too.
…ngs over

The destination id is now the bridge address alone. Rotating the token on
the same address re-points recordings still waiting to the new token, instead
of leaving them refused until they expire. A different address is still a
different destination and never receives them.
…line

A new token for a known bridge now lifts the day-long refusal delay, so
files refused under the old token are tried again on the next pass.

The sync headline no longer reads "all recordings" or "nothing waiting"
while some recordings have no secure destination and can never sync.

The stuck-recording alert is raised or cleared by what is actually stuck,
on every pass: it is cleared once nothing is stuck, and a pass that
finished no longer clears an alert that still applies.
The sync pass no longer deletes anything. A week after the edge confirms a
recording it still asks once whether the edge holds those exact bytes: a
yes leaves an X.checked mark, a no puts the file back in the upload queue.

Retention now owns every delete. Anything older than 90 days goes. Under
storage pressure, copies the edge confirmed may go early (re-confirmed
ones first, oldest first), only until there is room again. Every synced
copy carries the edge's hash-verified receipt, so nothing is deleted early
that the edge never held. An unsynced recording is the only copy and is
never deleted early: a full phone turns the local backup off instead (the
previous 45-day low-storage floor is removed).

Version 0.6.1.
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e313767f-b3a6-4c17-9d3e-9f080498a470
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T18:49:23.336053Z 9242801 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee2d73fcf8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +125 to +129
RetentionPolicy.sweep(segments, nowUtcMs, hasRoom) { s ->
val f = files.getValue(s.id)
if (!f.exists()) continue // renamed by sync since the listing; judged next time
// Renamed by sync since the listing (or already gone): judged next time. This is what
// keeps a copy the edge disowned (renamed back to X.aac) from going as a held one.
if (!f.exists()) return@sweep

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent purge from racing a negative edge recheck

When storage is low and /have reports false, runPass receives that answer before acquiring folderLock to rename the recording back to .aac. A concurrent purge can acquire the lock first, see the still-existing .synced.aac here, and delete it as an early-retention candidate; the subsequent rename then finds no file, leaving neither the edge nor the phone with the recording. The existence check only handles a rename that has already completed, so the negative recheck and purge deletion need coordination that covers this window.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9242801: the pass claims the recording (SegmentSync.verifying) under folderLock before calling /have and releases it only after acting on the answer; purgeDir skips any claimed recording under the same lock. A "no" therefore always finds the file and re-queues it. Every sync call also has a 10-minute overall timeout so a claim cannot be held indefinitely. Tests: aRecordingIsClaimedAgainstPurgeWhileTheEdgeIsAsked, purgeLeavesARecordingAloneWhileItsEdgeCopyIsBeingRechecked.

…ng or the queue

A failure to schedule the sync job when a segment closes is logged instead
of being read as an encoder failure that paused the local backup.

An unreadable sidecar holds back only its own file. After a day that
recording uploads without its metadata, so the phone never keeps the only
copy because of a broken sidecar.
…-90d

# Conflicts:
#	clients/android-mic/app/build.gradle.kts
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/LocalRecorder.kt
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/MainActivity.kt
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/RetentionPolicy.kt
#	clients/android-mic/app/src/main/java/com/genesis/meetingmic/SegmentSync.kt
#	clients/android-mic/app/src/test/java/com/genesis/meetingmic/LocalBackupTest.kt
#	clients/android-mic/app/src/test/java/com/genesis/meetingmic/SegmentSyncPassTest.kt
#	clients/android-mic/app/src/test/java/com/genesis/meetingmic/SegmentSyncTest.kt
@WingedGuardian
WingedGuardian changed the base branch from feat/meeting-mic-sync to main October 23, 2026 18:23
…-checked

While the sync pass asks the edge whether it still holds a recording, a
low-storage purge could delete the phone copy just before an answer of
"no" put it back in the queue, leaving no copy anywhere. The pass now
claims the recording under the folder lock before asking, and purge
leaves any claimed recording alone. Every sync call also gets an overall
time limit, so a slow edge cannot hold a claim open indefinitely.
@WingedGuardian

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 92428011da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@WingedGuardian
WingedGuardian merged commit 327c09f into main Oct 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant