fix: repin siblings, per-day prune scoping, monotonic auto-continue clock, CI pin guard in the release path - #169
Conversation
…o-continue clock Repin protocol -> 5bb8606 and analytics -> 5d42191 (both were one merge behind main). Concretely: protocol's crc8-on-length-field check (7269ef9) was merged a full day before this repin — until now, edge's pinned FrameReassembler had no check on the declared frame length before trusting it, so one corrupted length byte during a historical offload could consume up to ~4092 bytes of good stream (data the strap was about to trim from flash and would never resend). This shipped unpinned in every build (CI and any release) cut from the old pin. pruneSupersededIntermediates() kept the 2 highest algo_version values PRESENT ANYWHERE in sleep_session_candidates/wake_day_features, table- wide rather than per day_id. A day whose raw substrate already aged out (3-day retention) never re-enters the derive pipeline to write a newer- version row of its own, so the moment any two OTHER days reached the next two kAlgoVersion generations, that day's only cached row got deleted -- orphaning a day that can never be re-derived. Scoped the retention per day_id instead. AutoContinueRun's elapsed-time ceiling was measured with DateTime.now() -- real wall-clock, not monotonic. A clock correction (DST fall-back, NTP) mid-chain could make elapsed() return a small or negative value and silently disable the one time-based cap standing between a stuck-but- churning offload chain and pausing heartbeat/keepalive/RTC-reverify for much longer than intended. BleEngine now feeds it a dedicated Stopwatch- backed monotonic clock instead; AutoContinueRun's own (now-verified-safe) double-seconds interface is untouched, so its existing deterministic tests still drive it with fake values. Reviewed but NOT changed: the auto-continue "productive" streak can be reset by any HISTORY_END token that merely differs from the immediately previous one, with no monotonicity guarantee on that token. Whether this is actually exploitable rests on an assumption about firmware continuation-token behavior I can't verify without real hardware, and a speculative change to that gate risks a worse, concrete regression (treating a legitimate later batch as already-seen and under-draining real backlog) in exchange for a hypothetical one. The monotonic-clock fix above already makes the 10-minute ceiling the design intends as the real backstop actually enforce itself. CI: the pin-drift guard only ran in test.yml, which a tag-triggered release never executes -- so it protected nothing about an actual release. Extracted it to .github/scripts/check_sibling_pins.sh and wired it into build.yml's preflight too (which both release jobs depend on). Also extended it with a non-fatal staleness check against each sibling's live GitHub main (git ls-remote) -- the internal lock-vs-yaml check alone passes clean on a pin that's simply gone stale, which is exactly what happened here. Verified against both the fatal (path-source) and warning (staleness) paths with synthetic fixtures before wiring it in, including replaying this exact incident's before/after SHAs through it.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR centralizes sibling dependency pin checks in CI, updates sibling commit pins, uses monotonic elapsed time for BLE auto-continuation limits, and changes intermediate database pruning to retain versions independently per day. ChangesSibling dependency pin validation
Monotonic continuation timing
Scoped intermediate pruning
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
PR Reviewer Guide 🔍(Review updated until commit 7e83b15)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 7e83b15
Previous suggestionsSuggestions up to commit 4b055f4
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/data/db.dart (1)
4560-4589: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valuePer-day scoping fix looks correct. The
versionsByDaymap correctly isolates each day's own version history, so a day whose raw substrate aged out and can never write a newer generation is no longer collateral damage from unrelated days advancing — matches the new regression test's exact scenario.One trade-off worth noting: this now issues one
db.deleteper day requiring pruning instead of the old single table-wide delete, an N+1 pattern. Givensleep_session_candidates/wake_day_featuresare small and most days settle at ≤keepVersionsafter steady state, this is likely fine, but a large backlog (e.g., first run after many historical algo bumps) could issue many round trips.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/data/db.dart` around lines 4560 - 4589, The per-day pruning logic in pruneSupersededIntermediates is correct; retain the versionsByDay scoping and current behavior. No change is required for the noted per-day delete round trips unless performance optimization is specifically needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/check_sibling_pins.sh:
- Around line 75-83: Bound the git ls-remote probe in the upstream check around
the existing repo and refs/heads/main arguments by running it through the
repository’s available timeout utility, with a finite duration. Preserve the
current successful pin comparison and fallback warning behavior when the command
times out or otherwise fails.
In `@lib/ble/ble_engine.dart`:
- Around line 2572-2578: Add an engine-level regression test covering the
auto-continue policy path around the 599/600-second boundary, using an
injectable elapsed-time provider or equivalent clock control to supply monotonic
readings while wall time moves backward. Verify the ceiling remains enforced and
the behavior is driven by the monotonic value passed through the BleEngine
auto-continue flow, reusing existing test setup and symbols in
test/sync_policy_test.dart where applicable.
---
Outside diff comments:
In `@lib/data/db.dart`:
- Around line 4560-4589: The per-day pruning logic in
pruneSupersededIntermediates is correct; retain the versionsByDay scoping and
current behavior. No change is required for the noted per-day delete round trips
unless performance optimization is specifically needed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 19dde18e-14be-437e-a3a4-21b92aa77396
⛔ Files ignored due to path filters (1)
pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
.github/scripts/check_sibling_pins.sh.github/workflows/build.yml.github/workflows/test.ymllib/ble/ble_engine.dartlib/data/db.dartpubspec.yamltest/db_storage_hygiene_test.dart
Both from CodeRabbit's review of this branch: - check_sibling_pins.sh's git ls-remote had no timeout. The whole staleness probe is designed to degrade to a warning on any network trouble, but an unbounded hang would instead wedge the job (test, or worse, release preflight) until CI's own multi-hour timeout -- a much bigger failure than the check it's guarding against. Wrapped in `timeout --kill-after=5s 30s`. - pruneSupersededIntermediates issued one DELETE per day_id with no wrapping transaction. Right after a kAlgoVersion bump forces a bulk re-derive (or "Re-analyze data"), many days can cross the keepVersions threshold in the same pass. Wrapped each table's per-day loop in db.transaction(...), matching the sibling pruneDecodedBeforeRecTs's existing pattern -- verified it isn't nested inside an outer transaction (the two prune calls in _pruneOldDecoded run sequentially, each owning its own). Not changed, and replied on the PR explaining why: a third comment flagged a possible wall-clock/monotonic-clock mix in AutoContinueRun's elapsed computation. Traced it -- _startedAt is only ever seeded inside continued(), and both call sites already pass _monotonicSecs() (grepped every _autoContinue.* use site to confirm) -- so there's no wall-clock path left to mix in. False alarm; the bot flagged it appropriately given it couldn't see AutoContinueRun's definition in this diff.
|
Went through the PR-Agent findings too:
Also replied inline to both CodeRabbit findings — one confirmed and fixed (the |
|
Persistent review updated to latest commit 7e83b15 |
User description
Summary
5bb8606, analytics →5d42191(both were one merge behind main). Concretely: protocol's CRC8-on-length-field check (7269ef9) merged a full day before this repin — until now, edge's pinnedFrameReassemblerhad no check on the declared frame length before trusting it, so one corrupted length byte during a historical offload could consume up to ~4092 bytes of good stream (data the strap was about to trim from flash and would never resend). This shipped unpinned in every CI run and any release cut from the old pin.pruneSupersededIntermediates()was table-wide, not per-day. It kept the 2 highestalgo_versions present anywhere insleep_session_candidates/wake_day_features. A day whose raw substrate already aged out (3-day retention) never re-enters the derive pipeline to write a newer-version row of its own — so the moment any two other days reached the next twokAlgoVersiongenerations, that day's only cached row got deleted, orphaning a day that can never be re-derived. Scoped retention perday_idinstead.AutoContinueRun's time ceiling usedDateTime.now()— real wall-clock, not monotonic. A DST fall-back or NTP correction mid-chain could makeelapsed()go small/negative and silently disable the one time-based cap standing between a stuck-but-churning offload chain and pausing heartbeat/keepalive/RTC-reverify for far longer than intended.BleEnginenow feeds it a dedicatedStopwatch-backed monotonic clock;AutoContinueRun's own double-seconds interface is untouched so its existing deterministic tests are unaffected.test.yml, which a tag-triggered release never executes — so it protected nothing about an actual release. Extracted to.github/scripts/check_sibling_pins.sh, wired intobuild.yml'spreflight(which both release jobsneeds:) too. Also added a non-fatal staleness check against each sibling's live GitHubmainviagit ls-remote— the internal lock-vs-yaml check alone passes clean on a pin that's simply gone stale, which is exactly what happened here.Reviewed, deliberately NOT changed
The auto-continue "productive" streak resets on any HISTORY_END token that merely differs from the immediately-previous one, with no documented monotonicity guarantee on that token. Whether this is actually exploitable rests on an assumption about firmware continuation-token behavior I can't verify without real hardware — a speculative change to that gate risks a worse, concrete regression (treating a legitimate later batch as already-seen and under-draining real backlog) in exchange for fixing a hypothetical one. The monotonic-clock fix above already makes the 10-minute ceiling — the real backstop by design — actually enforce itself reliably.
Known follow-up
This pin predates protocol
fix/issuesPR #21 (realtimeRr RR-bound, historical-family activity/steps_inc null-vs-0) — repin again once that merges.Test plan
flutter test --concurrency=1— 1055 passed (added a per-day-prune regression test that reproduces the exact orphaning scenario)flutter analyze— clean.github/scripts/check_sibling_pins.shmanually verified against both its fatal (path-source) and warning (staleness) paths with synthetic fixtures, including replaying this incident's actual before/after SHAs through itpubspec.lockregenerated withpubspec_overrides.yamlmoved aside first (restored after), so it reflects the real git pins, not local pathsPR Type
Bug fix, Tests, Enhancement
Description
Repin protocol (
5bb8606) and analytics (5d42191) to pick up CRC8 length-field check preventing up to 4092 bytes of stream data loss on corrupted framesFix
pruneSupersededIntermediates()to scope retention perday_idinstead of table-wide, preventing orphaned rows for days that can never be re-derivedReplace wall-clock
DateTime.now()with monotonicStopwatchforAutoContinueRunelapsed ceiling, preventing DST/NTP jumps from silently disabling the capExtract sibling-pin guard to shared script and wire it into
build.ymlpreflight so release tag builds are also protectedDiagram Walkthrough
File Walkthrough
2 files
Add monotonic Stopwatch clock for auto-continue elapsed ceilingScope intermediate prune retention per day_id not table-wide1 files
Add regression test for per-day prune orphan bug3 files
Extract shared sibling-pin guard script with staleness warningWire pin-drift guard into release preflight jobReplace inline pin guard with shared script invocation1 files
Repin protocol and analytics to current main SHAsSummary by CodeRabbit