Skip to content

Commit 051f1e3

Browse files
committed
fix(workouts): stop re-derivation wiping user-confirmed/corrected types
detectStoreDay and processUser both DELETE auto sessions before re-inserting, and setWorkoutType leaves source='auto' — so a confirmed/corrected workout was deleted on every re-derive (the */10 sweep, every close_day) and reset to the model's guess, erasing the calibration ledger within ~10 min. The INSERT's ON CONFLICT preserve-corrected clause never fired because the row was already gone in the same batch transaction. Exclude type_source IN ('confirmed','corrected') from both DELETEs so the labelled row survives (ON CONFLICT then preserves type/confidence while refreshing the model metrics), and add it to the overlap-skip set so a baseline-drift start_ts shift can't insert a duplicate model row over the user's label.
1 parent 884540e commit 051f1e3

2 files changed

Lines changed: 21 additions & 7 deletions

File tree

src/analytics.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,11 +390,22 @@ export async function processUser(
390390
// user-owned and must survive a re-derive. Deleted tombstones are KEPT so a
391391
// user-deleted auto session isn't resurrected (its row's status stays
392392
// 'deleted'; the re-insert below only updates non-status fields via ON CONFLICT).
393+
// ALSO preserve any auto session the user CONFIRMED/CORRECTED (the calibration
394+
// ledger): exclude it from the DELETE and skip an overlapping re-detection so a
395+
// baseline-drift start_ts shift can't insert a duplicate that re-overwrites the
396+
// user's label. (Without this, every re-derive wiped user corrections.)
397+
const { results: keepRev } = await db.prepare(
398+
"SELECT start_ts, end_ts FROM sessions WHERE user_id = ? AND start_ts < ? AND end_ts > ? AND status != 'deleted' AND type_source IN ('confirmed','corrected')",
399+
).bind(userId, dayStart + DAY, dayStart).all<{ start_ts: number; end_ts: number }>()
400+
const reviewed = (keepRev ?? []) as { start_ts: number; end_ts: number }[]
401+
const overlapsReviewed = (s: { start_ts: number; end_ts: number }) =>
402+
reviewed.some((k) => s.start_ts < k.end_ts && s.end_ts > k.start_ts)
393403
statements.push(
394-
db.prepare("DELETE FROM sessions WHERE user_id = ? AND start_ts >= ? AND start_ts < ? AND (source IS NULL OR source = 'auto') AND status != 'deleted'")
404+
db.prepare("DELETE FROM sessions WHERE user_id = ? AND start_ts >= ? AND start_ts < ? AND (source IS NULL OR source = 'auto') AND COALESCE(type_source,'model') NOT IN ('confirmed','corrected') AND status != 'deleted'")
395405
.bind(userId, dayStart, dayStart + DAY),
396406
)
397407
for (const s of sessions) {
408+
if (overlapsReviewed(s)) continue
398409
const sid = `${userId}:${s.start_ts}`
399410
statements.push(db.prepare(
400411
'INSERT INTO sessions (user_id, id, start_ts, end_ts, type, avg_hr, max_hr, strain, calories, hrr60, zones, confidence, status, source, segments, detected_type, type_confidence, type_source) ' +

src/workouts.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -385,18 +385,21 @@ async function detectStoreDay(db: D1Database, userId: string, dayStart: number,
385385
act_class: r.act_class as Minute['act_class'],
386386
}))
387387
const sessions = detectSessions(dayMin, baseline, profile)
388-
// Reconcile: don't clobber the user's manual workouts, deleted tombstones, OR any
389-
// already-typed live-detected sessions (source='auto_live'); only the minute-detector's
390-
// own 'auto' rows are re-derived. Then skip auto bouts that overlap a manual/auto_live
391-
// session so a live-streamed workout isn't double-counted by the backstop.
388+
// Reconcile: don't clobber the user's manual workouts, deleted tombstones, any
389+
// already-typed live-detected sessions (source='auto_live'), OR any auto session the
390+
// user CONFIRMED/CORRECTED (type_source in confirmed/corrected — the calibration
391+
// ledger). Only the minute-detector's own un-reviewed 'auto' rows are re-derived. Then
392+
// skip auto bouts that overlap any preserved session so a live-streamed or
393+
// user-labelled workout isn't double-counted (and a baseline-drift start_ts shift can't
394+
// spawn a duplicate that re-overwrites the user's label).
392395
const { results: keep } = await db.prepare(
393-
"SELECT start_ts, end_ts FROM sessions WHERE user_id = ? AND start_ts < ? AND end_ts > ? AND status != 'deleted' AND source IN ('manual','auto_live')",
396+
"SELECT start_ts, end_ts FROM sessions WHERE user_id = ? AND start_ts < ? AND end_ts > ? AND status != 'deleted' AND (source IN ('manual','auto_live') OR type_source IN ('confirmed','corrected'))",
394397
).bind(userId, dayStart + DAY, dayStart).all<{ start_ts: number; end_ts: number }>()
395398
const covered = (keep ?? []) as { start_ts: number; end_ts: number }[]
396399
const overlaps = (s: { start_ts: number; end_ts: number }) =>
397400
covered.some((k) => s.start_ts < k.end_ts && s.end_ts > k.start_ts)
398401
const stmts: D1PreparedStatement[] = [
399-
db.prepare("DELETE FROM sessions WHERE user_id = ? AND start_ts >= ? AND start_ts < ? AND (source IS NULL OR source = 'auto') AND status != 'deleted'")
402+
db.prepare("DELETE FROM sessions WHERE user_id = ? AND start_ts >= ? AND start_ts < ? AND (source IS NULL OR source = 'auto') AND COALESCE(type_source,'model') NOT IN ('confirmed','corrected') AND status != 'deleted'")
400403
.bind(userId, dayStart, dayStart + DAY),
401404
]
402405
for (const s of sessions) {

0 commit comments

Comments
 (0)