Skip to content

feat(app): support ring-buffer offline sync for fw 3.0.20+#7218

Open
mdmohsin7 wants to merge 12 commits into
mainfrom
charles/ring-buffer-sync
Open

feat(app): support ring-buffer offline sync for fw 3.0.20+#7218
mdmohsin7 wants to merge 12 commits into
mainfrom
charles/ring-buffer-sync

Conversation

@mdmohsin7
Copy link
Copy Markdown
Member

Summary

App-side support for the ring-buffer NAND storage protocol introduced in firmware PR #7216 (TuEmb). Devices on fw >= 3.0.20 will use this path; fw 3.0.17–.19 keep the existing multi-file LittleFS path; fw < 3.0.17 stay on the legacy SD-card path.

The 444-byte record format ([ts:4 BE][audio:440]) is identical to the multi-file protocol on main — only the control plane changes (read by u64 sequence, advance by sequence). The audio parser is reused unchanged.

Commits (atomic)

  1. Add RingStatus / RingInfo data classes + abstract method surface on DeviceConnection
  2. Implement ring-buffer BLE protocol on OmiDeviceConnection (CMD_RING_INFO/READ/ADVANCE/CLEAR + NOTIFY_INFO/ACK/READ_BEGIN/DATA/DONE handling)
  3. Add RingStorageSyncImpl mirroring StorageSyncImpl but talking ring (NOTIFY_DATA reassembly across BLE chunks, advance only after LocalWalSync confirms chunk landed)
  4. Gate Phase-0 sync in WalSyncs.syncAll() by firmware version; route _checkAndStartAutoSync through getRingStatus() for ring-capable devices
  5. Refactor — extract pure-data wire helpers into RingProtocol so they're testable without BLE mocking
  6. 30 unit tests covering status / NOTIFY_INFO / NOTIFY_DONE / NOTIFY_READ_BEGIN parsers, CMD_RING_READ / CMD_RING_ADVANCE u64 BE encoding, packed audio payload framing, and the BLE chunk reassembler

Data-safety invariant

CMD_RING_ADVANCE is sent only after NOTIFY_DONE arrives with status=0 and every chunk has been registered with LocalWalSync. Any failure (cancel, BLE drop, flush error, non-zero DONE status) leaves the ring untouched — the next sync resumes from the same read_seq. This is the data-loss win versus the file-based protocol's delete-after-read semantics.

RTC handling

If status.rtc_valid == 0 (device RTC not yet synced via performSyncTime), per-record timestamps are treated as unreliable and timerStart falls back to now - estimated_duration, matching the legacy code path. If rtc_valid == 1, the first record's BE u32 timestamp anchors timerStart.

Out of scope

  • WiFi sync path — unchanged (no longer in user-facing use)
  • Backend / cloud upload — LocalWalSync is reused as-is; no API changes

Test plan

  • Local: cd app && flutter test test/unit/ring_protocol_test.dart — 30/30 pass
  • Local: full flutter analyze — 0 new issues introduced (pre-existing warnings on main untouched)
  • Dev node: connect a fw 3.0.20 device, verify auto-sync banner triggers on connect when ring has unread packets, verify download completes and ring advances
  • Dev node: cancel sync mid-stream, verify ring is NOT advanced, verify next sync resumes from same read_seq
  • Dev node: connect a fw 3.0.17–.19 device (existing multi-file path), verify behavior unchanged
  • Dev node: connect a fw < 3.0.17 device (legacy SD card path), verify behavior unchanged

Closes nothing yet (firmware PR #7216 is the dependency).

mdmohsin7 added 6 commits May 10, 2026 09:41
…viceConnection

Adds data classes and abstract method surface for the ring-buffer storage
protocol (firmware 3.0.20+, omi PR #7216). Same BLE service UUID as the
existing multi-file protocol; subclasses fill in the new opcodes.

The 444-byte record format ([ts:4 BE][audio:440]) is identical to the
multi-file protocol on main, so the audio parser will be reused. Only
the control-plane (read-by-sequence, advance-by-sequence) is new.
Implements the wire protocol for fw 3.0.20+ ring storage on the existing
30295780 service:

- performGetRingStatus: 16-byte read of status char (used/unread/free/rtcValid)
- performGetRingInfo: CMD_RING_INFO (0x10) on control char, awaits NOTIFY_INFO
  (0x02) and decodes the 31-byte payload (u64/u64/u32/u64/u16 BE)
- performReadRingFromSeq: CMD_RING_READ (0x11) with u64 BE start_seq and
  optional u32 BE packet_count; data flows through the existing storage
  bytes listener (NOTIFY_DATA chunks) handled by the sync layer
- performAdvanceRing: CMD_RING_ADVANCE (0x12) with u64 BE seq, awaits
  NOTIFY_ACK (0x01) so callers can verify the commit succeeded before
  treating the chunk as durably consumed
- performClearRing: CMD_RING_CLEAR (0x13), awaits NOTIFY_ACK
Mirrors StorageSyncImpl but talks the ring protocol from omi PR #7216:

- refreshWalsFromDevice: 16B status read; emits ONE virtual Wal covering
  the entire unread region (the ring is a single logical stream, not a
  list of files). Skipped when <10s of audio, mirroring the file-based
  threshold.
- _syncRing: snapshots ring state via getRingInfo, sends CMD_RING_READ,
  demuxes notifications by opcode (ACK/INFO/DATA/DONE/READ_BEGIN), and
  reassembles the unaligned NOTIFY_DATA byte stream into 444B records.
  Per record, strips the 4B BE timestamp and feeds the 440B audio
  payload through the same packed [size:1][frame:size] parser used by
  the multi-file path.
- Chunks flush to disk + register with LocalWalSync as we go (60s
  chunks per sdcardChunkSizeSecs) so a mid-stream BLE drop still lands
  the early audio in the cloud.
- CMD_RING_ADVANCE is sent ONLY after NOTIFY_DONE arrives with status=0
  AND every chunk has been registered with LocalWalSync. Any failure
  (cancel, BLE drop, flush error, DONE with non-zero status) leaves the
  ring untouched — the next sync resumes from the same read_seq. This
  is the data-safety invariant.
- rtc_valid handling: if the device RTC isn't synced yet, fall back to
  now-duration like the legacy code does. Otherwise the per-record
  timestamp from the first record anchors timerStart.
- dropped_packets > 0 surfaces as a DebugLogManager warning; sync
  proceeds with whatever the firmware still has.
Routes the offline storage sync to the right protocol based on firmware
version, with no impact on devices outside the ring-firmware range:

- WalSyncs.isRingBufferFirmware(version) static helper for the >=3.0.20
  cutover, parallel to DeviceProvider._isFirmwareVersionSupported (>=3.0.17)
  for the multi-file path.
- New WalSyncs.setDevice() so the sync orchestrator knows the firmware
  version at syncAll() time without re-querying BLE.
- Phase 0 in syncAll() now branches: fw>=3.0.20 calls _ringSync, fw 3.0.17-.19
  keeps the existing _storageSync path. fw<3.0.17 falls through to Phase 1a
  legacy SD-card sync as before.
- _ringSync threaded through deleteWal, deleteAllSynced/Pending, getMissing,
  getAll, start, stop, cancelSync, isStorageSyncing, storageSpeedKBps so
  the rest of the codebase observes ring sync state through the same
  surfaces it already uses for storage sync.

DeviceProvider:
- syncs.setDevice(device) called alongside the existing per-sync fanout
  so WalSyncs has the firmware version locally.
- syncs.ring.setDevice(device) so the ring path can read its device.
- _checkAndStartAutoSync branches on isRingBufferFirmware: ring-capable
  devices use connection.getRingStatus() (16B status read) instead of
  the multi-file file-list endpoint, which the ring firmware no longer
  serves. Without this, the auto-sync banner would never appear on
  fw>=3.0.20 devices.
Hoists status / NOTIFY_INFO / NOTIFY_DONE / NOTIFY_READ_BEGIN parsing,
CMD_RING_READ / CMD_RING_ADVANCE encoding, the 4-byte BE timestamp
extractor, and the [size:1][frame:size] audio-payload parser into a
new RingProtocol module. Adds RingRecordReassembler for the unaligned
NOTIFY_DATA byte stream.

OmiDeviceConnection and RingStorageSyncImpl now call into these
helpers instead of inlining ByteData ops. No behavior change — the
helpers are byte-for-byte equivalent to the previous inline parsers.

The split makes the wire protocol unit-testable without BLE mocking;
tests are added in the next commit.
30 tests covering:
- 16-byte status read parse (4x u32 LE) including rtc_valid surfacing
- NOTIFY_INFO (0x02), NOTIFY_DONE (0x04), NOTIFY_READ_BEGIN (0x05) parsers
  including opcode and length validation
- CMD_RING_READ encoding with and without packet_count, u64 BE on the wire
- CMD_RING_ADVANCE encoding, u64 BE
- 4-byte BE timestamp extraction
- packed [size:1][frame:size] audio payload parsing including zero-padding,
  multi-frame, truncated trailing frame
- RingRecordReassembler boundary, mid-record split, multi-record drain,
  byte-by-byte streaming, and a stream of two records with a mid-record cut
- end-to-end: a 444B record split across two NOTIFY_DATA chunks reassembles
  cleanly and decodes the audio payload
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 10, 2026

Greptile Summary

This PR adds app-side support for the ring-buffer NAND storage protocol introduced in firmware 3.0.20, routing devices on older firmware versions to the existing LittleFS or SD-card paths based on the firmware version comparison in WalSyncs.isRingBufferFirmware.

  • RingProtocol provides testable, BLE-free wire helpers; RingStorageSyncImpl reassembles unaligned BLE chunks into 444-byte records, extracts Opus frames, writes them to disk, and advances the ring pointer only after NOTIFY_DONE with status=0 is confirmed.
  • DeviceProvider and WalSyncs wire the new sync path into the existing Phase-0 slot, gated by the firmware version stored on the connected device.
  • 30 unit tests cover protocol parsing and the BLE chunk reassembler, but two logic bugs in RingStorageSyncImpl.syncAll() and the unawaited flush path require attention before merging.

Confidence Score: 3/5

Two correctness bugs in the new sync path should be fixed before merging: one causes failed syncs to be silently marked complete (breaking retries), the other can assign identical timestamps to consecutive audio chunks.

The WAL-status inversion in syncAll() means any interrupted ring transfer will be recorded as done rather than retried, directly contradicting the data-safety invariant the PR documents. The unawaited flush concurrency issue means multiple chunks arriving during a slow disk write can race on the shared chunkTimerStart variable, producing overlapping audio segments in LocalWalSync. Both bugs live on the hot path of every ring sync session.

app/lib/services/wals/ring_storage_sync.dart warrants the most scrutiny — it contains both correctness issues and drives the entire ring sync flow.

Important Files Changed

Filename Overview
app/lib/services/wals/ring_storage_sync.dart New 622-line ring-buffer sync implementation; contains a WAL status bug (synced before completion check) and a concurrent-flush race on chunkTimerStart.
app/lib/services/devices/ring_protocol.dart Pure-data BLE wire helpers + RingRecordReassembler; off-by-one in parseAudioPayload (>= vs >), but otherwise well-tested by 30 unit tests.
app/lib/services/devices/omi_connection.dart Adds performGetRingStatus/Info/Read/Advance/Clear overrides; uses correct UUIDs matching the multi-file protocol characteristics. Looks correct.
app/lib/services/devices/device_connection.dart Adds RingStatus/RingInfo data classes and abstract ring-buffer method stubs; logic is clean and non-breaking.
app/lib/services/wals/wal_syncs.dart Adds RingStorageSyncImpl wiring, isRingBufferFirmware version gate, and Phase-0 routing logic; the firmware version comparison is correct.
app/lib/providers/device_provider.dart Minimal additions: sets the device on the ring sync, and routes auto-sync detection through getRingStatus() for ring-capable firmware.
app/lib/services/wals/wal_interfaces.dart Adds RingStorageSync abstract interface mirroring StorageSync; non-breaking and clean.
app/test/unit/ring_protocol_test.dart 30 unit tests covering protocol parsing, command encoding, and reassembly; well-structured, though the off-by-one edge case in parseAudioPayload is untested.

Sequence Diagram

sequenceDiagram
    participant DP as DeviceProvider
    participant WS as WalSyncs
    participant RS as RingStorageSyncImpl
    participant OC as OmiDeviceConnection
    participant FW as Firmware (3.0.20+)

    DP->>WS: setDevice(device)
    DP->>WS: syncAll()
    WS->>WS: isRingBufferFirmware(fwVersion)?
    alt "fw >= 3.0.20 (ring path)"
        WS->>RS: refreshWalsFromDevice()
        RS->>OC: getRingStatus()
        OC->>FW: Read status char (30295782)
        FW-->>OC: 16-byte LE [used, unread, free, rtcValid]
        OC-->>RS: RingStatus
        WS->>RS: syncAll()
        RS->>OC: getRingInfo()
        OC->>FW: Write CMD_INFO (0x10)
        FW-->>OC: NOTIFY_INFO (0x02)
        RS->>OC: readRingFromSeq(read_seq)
        OC->>FW: Write CMD_RING_READ (0x11)
        FW-->>OC: NOTIFY_READ_BEGIN (0x05)
        loop NOTIFY_DATA chunks
            FW-->>OC: NOTIFY_DATA (0x03)[raw_bytes...]
            OC-->>RS: onStorageBytesReceived
            RS->>RS: reassembler.append() then drainRecords()
            RS->>RS: flushChunks() then _flushToDisk + _registerWithLocalSync
        end
        FW-->>OC: NOTIFY_DONE (0x04)[status, next_seq]
        alt "status==0 AND no flush error"
            RS->>OC: advanceRing(next_seq)
            OC->>FW: Write CMD_RING_ADVANCE (0x12)
            FW-->>OC: NOTIFY_ACK (0x01)[status]
        else failure
            RS->>RS: skip advance, ring unchanged
        end
    else fw 3.0.17-3.0.19
        WS->>WS: StorageSync.syncAll()
    end
Loading

Reviews (1): Last reviewed commit: "test(app): unit tests for ring-buffer wi..." | Re-trigger Greptile

@mdmohsin7
Copy link
Copy Markdown
Member Author

morpheus review — omi#7218 (ring-buffer sync, fw 3.0.20+)

Scope: 8 files, +1486/-17, 6 atomic commits, 30 unit tests. App-side ring-buffer protocol for the new NAND storage firmware. Clean commit structure, no co-author trailers, no forward-references.

P2 — deleteWal unconditionally clears the ring (potential data loss)

RingStorageSyncImpl.deleteWal calls _clearRingOnDevice() before filtering the wal list, regardless of whether the wal being deleted belongs to the ring sync:

// ring_storage_sync.dart, deleteWal()
@override
Future deleteWal(Wal wal) async {
    await _clearRingOnDevice();  // ← fires for ANY wal, not just ring wals
    _wals = _wals.where((w) => w.id != wal.id).toList();
    listener.onWalUpdated();
}

WalSyncs.deleteWal cascades to all sync types (line 53: await _ringSync.deleteWal(wal)). So deleting a phone-sync or sdcard wal from the UI (auto_sync_page, wal_item_detail_page, sync_page) will send CMD_RING_CLEAR to a connected ring-capable device, wiping unread data.

Compare with the adjacent StorageSyncImpl.deleteWal (storage_sync.dart:220): it calls _deleteWalsOnDevice([wal]), which guards on wals.isEmpty and filters to fileNum >= 0 — a no-op for non-storage wals.

Fix: guard the clear with a membership check:

@override
Future deleteWal(Wal wal) async {
    if (_wals.any((w) => w.id == wal.id)) {
        await _clearRingOnDevice();
    }
    _wals = _wals.where((w) => w.id != wal.id).toList();
    listener.onWalUpdated();
}

Same guard needed on deleteAllPendingWals — currently unconditionally clears the ring even when _wals has no pending ring wals.

Verified (no issues)

Data-safety invariant: CMD_RING_ADVANCE is gated on five conditions (reachedDone, doneOk, !flushError, !_isCancelled, doneNextSeq != null). The unawaited flush pattern in _syncRing is safe under Dart's cooperative concurrency — removeRange happens synchronously before any await, so each chunk is owned by exactly one flush task. flushError is set before rethrow, so it's visible by the time advancedOk is evaluated.

Wire protocol: Status read from 30295782 (LE u32x4), commands/notifications on 30295781 (BE payloads) — matches the firmware doc layout. parseInfoNotification correctly validates opcode + minimum length before parsing.

Version gate: isRingBufferFirmware correctly implements >= 3.0.20 semver comparison, parallel to the existing _isFirmwareVersionSupported >= 3.0.17 check.

Reassembler: RingRecordReassembler correctly handles unaligned BLE chunks. Buffer management is straightforward — append + drain at 444B boundaries. Well covered by tests (boundary, mid-split, multi-record, byte-by-byte, two-record cross-cut).

Adjacent pattern comparison: ring sync mirrors storage sync faithfully — refreshWalsFromDevice creates virtual wals with the same threshold (10s), stopStorageSync() before discovery, chunk flush + register cycle, speed tracking, progress reporting, cancel handling.

parseAudioPayload boundary check: offset + 1 + size >= audio.length uses >= (not >) — a frame that exactly fills remaining space is treated as truncated. This is inherited from the existing parser (storage_sync.dart:486) and is consistent.

Firmware version routing in syncAll: Correctly branches ring vs multi-file in Phase 0 without affecting Phase 1a (legacy SD-card) or Phase 1b (phone sync).

Tests: 30 tests covering status/info/done/read_begin parsing, command encoding (u64 BE verified), timestamp extraction, audio payload parsing edge cases, reassembler boundary conditions, and an end-to-end chunk-split decode. Good coverage.

Status

Not approved — fix the P2 (deleteWal guard) before merge. The rest is solid.

Comment on lines +282 to +289
final complete = await _syncRing(wal, progress: progress);
wal.status = WalStatus.synced;
if (!complete) {
Logger.debug('RingStorageSync: Ring transfer incomplete; ring untouched, will resume next sync');
listener.onWalUpdated();
break;
}
listener.onWalUpdated();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 WAL is marked synced unconditionally before the completion check, so an incomplete transfer still removes the WAL from the retry queue. The next call to getMissingWals() (which filters on WalStatus.miss) will not see this WAL again, silently defeating the "resume from same read_seq" guarantee described in the PR.

Suggested change
final complete = await _syncRing(wal, progress: progress);
wal.status = WalStatus.synced;
if (!complete) {
Logger.debug('RingStorageSync: Ring transfer incomplete; ring untouched, will resume next sync');
listener.onWalUpdated();
break;
}
listener.onWalUpdated();
final complete = await _syncRing(wal, progress: progress);
if (!complete) {
Logger.debug('RingStorageSync: Ring transfer incomplete; ring untouched, will resume next sync');
listener.onWalUpdated();
break;
}
wal.status = WalStatus.synced;
listener.onWalUpdated();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in a792720 — wal.status only flips to synced after _syncRing returns true. On incomplete transfers the wal stays in WalStatus.miss so getMissingWals returns it on the next pass, pairing with the no-advance-on-failure guard so the ring also stays untouched.

Comment on lines +488 to +498
// Flush full chunks as we go (data safety: even if BLE drops mid-stream,
// already-flushed chunks land in LocalWalSync and reach the cloud).
if (bytesData.length >= chunkFrames) {
unawaited(() async {
try {
await flushChunks(finalFlush: false);
} catch (_) {
if (!completer.isCompleted) completer.complete(false);
}
}());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Concurrent flush races on chunkTimerStart

Each call to unawaited(() async { await flushChunks(...) }()) spawns an independent async closure that captures and mutates the same chunkTimerStart variable. Because flushChunks suspends at await _flushToDisk and await _registerWithLocalSync, a second BLE notification can arrive mid-flush, satisfy bytesData.length >= chunkFrames, and launch a second concurrent flush before the first one updates chunkTimerStart. Both flushes then pass the same chunkTimerStart value to _registerWithLocalSync, producing overlapping timestamps for consecutive audio chunks in LocalWalSync.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 7eb800c — replaced the unawaited closure with a single in-flight Future? inFlightFlush. New flushes only launch when the slot is free; the in-flight task's while loop absorbs additional NOTIFY_DATA arriving during its awaits. After NOTIFY_DONE / stream close, the post-loop code awaits the in-flight flush before draining the tail with finalFlush:true so the two never race on bytesData or chunkTimerStart.

Comment on lines +130 to +132
if (offset + 1 + size >= audio.length) {
break;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Off-by-one: the condition >= audio.length rejects a frame whose data ends exactly at the last byte of the buffer (offset + 1 + size == audio.length), even though the frame is fully present. For a 440-byte audio payload where a frame occupies the last byte exactly, the final frame is silently dropped. Should be > audio.length.

Suggested change
if (offset + 1 + size >= audio.length) {
break;
}
if (offset + 1 + size > audio.length) {
break;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1b98973 — boundary check now uses '>'. A frame whose data ends exactly at audio.length is parsed correctly. Added two tests: a 3-byte payload with one boundary-fitting frame, and a 440B payload tightly packed with 40 frames of 10 bytes (the 40th frame would have been silently dropped under '>='). Legacy parsers in storage_sync.dart and sdcard_wal_sync.dart kept on '>=' since they work for current codecs (opus 80B, opusFS320 160B never hit the boundary) and changing them risks regression in flows out of scope here.

WalSyncs.deleteWal and WalSyncs.deleteAllPendingWals fan out the call
to every sub-sync regardless of which one owns the wal. The ring impl
was unconditionally clearing the entire ring buffer on any cascading
delete, so deleting a phone or sdcard wal from the UI would wipe all
unsynced ring data the user didn't intend to delete.

Membership-guard both methods: deleteWal returns early if the wal id
isn't ours; deleteAllPendingWals returns early if we have no pending
wals.
@mdmohsin7
Copy link
Copy Markdown
Member Author

morpheus re-review — fix commit 635bb11

P2 resolved. Both deleteWal and deleteAllPendingWals now early-return when the wal/status doesn't belong to the ring sync, preventing cascading clears from WalSyncs. deleteAllSyncedWals correctly left untouched (no BLE command, list-only).

Approved. 7 commits, +1500/-17, 30/30 tests, analyzer clean.

mdmohsin7 added 3 commits May 10, 2026 10:38
syncAll set wal.status = WalStatus.synced unconditionally before
checking the result of _syncRing. An incomplete transfer (BLE drop,
non-zero NOTIFY_DONE status, cancel) was therefore removed from the
in-memory retry queue (getMissingWals filters on WalStatus.miss),
silently defeating the resume-from-read_seq guarantee for any UI
flow that relies on the cached _wals list between refreshes.

In practice the next refreshWalsFromDevice() call would have rebuilt
the list anyway, but the wal status flapping to 'synced' is observable
by listeners and contradicts the documented data-safety invariant.

Pair with the existing no-advance-on-failure guard in _syncRing — the
ring stays untouched on the device, and the wal stays in WalStatus.miss
on the phone.
The original NOTIFY_DATA handler launched a fresh unawaited flushChunks
closure every time bytesData crossed chunkFrames. Each closure
captures and mutates the shared chunkTimerStart variable, but
chunkTimerStart is only updated after the awaits inside flushChunks
return. Two concurrent flushes therefore both observed the same
chunkTimerStart value and passed it to _registerWithLocalSync,
producing overlapping timestamps for consecutive audio chunks.

Hold a single Future<void>? inFlightFlush reference. Only launch a
new flush when the slot is free; flushChunks's existing while-loop
naturally absorbs additional NOTIFY_DATA arriving during a flush on
its next iteration.

After NOTIFY_DONE / stream close, await the in-flight flush before
draining the tail with the final flushChunks(finalFlush: true) so
the two never race on bytesData or chunkTimerStart.
The previous boundary check 'offset + 1 + size >= audio.length' rejects
a frame whose data ends exactly at the last byte of the buffer, even
though the frame is fully present. For tightly-packed payloads where
(frameSize + 1) divides 440 evenly the final frame would be silently
dropped.

In practice no current codec hits the boundary — opus packs 5 80-byte
frames + padding (405B used) and opusFS320 packs 2 160-byte frames +
padding (322B used) — but a future codec with a 10-byte frame size
would lose every 40th frame.

Switch to '>'. The legacy parsers in storage_sync.dart and
sdcard_wal_sync.dart still use '>=' but are not changed: they're
working for current frame sizes and changing them risks regression
in flows we're not touching here.

Adds two unit tests: a 3-byte payload with one boundary-fitting frame,
and a 440-byte payload tightly packed with 40 frames of 10 bytes
(verifies the 40th frame is preserved).
@mdmohsin7
Copy link
Copy Markdown
Member Author

morpheus re-review — 3 fix commits (a792720, 7eb800c, 1b98973)

All three Greptile findings addressed correctly:

  1. synced-before-check (a792720): wal.status = WalStatus.synced now gated on complete == true. Incomplete transfers preserve miss status for retry. Pairs correctly with the no-advance invariant.

  2. flush timestamp race (7eb800c): Single inFlightFlush future replaces the unbounded unawaited pattern. New flushes gate on inFlightFlush == null; final flush awaits any in-flight flush before draining. finally { inFlightFlush = null } ensures the slot clears on error. This eliminates the concurrent-capture race on chunkTimerStart.

  3. off-by-one (1b98973): >= to > with comment explaining the legacy divergence. Two new tests cover the boundary case. Scoped correctly — legacy parsers left alone.

32/32 tests, analyzer clean, 10 atomic commits. Approved.


Self-note: I explicitly reviewed the flush concurrency and called it safe in my first pass. I traced data ownership correctly (removeRange is sync) but missed the timestamp race (chunkTimerStart update happens after the yield). Lesson: trace all shared mutable state through yield points, not just the primary data structure.

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