A sync pass is modelled as a side effect rather than as a function that returns what it did. That single choice is why sync failures are structurally invisible, and it is cheap to reverse.
Today
The information mostly already exists and is discarded. sync_one computes new_events_count per bucket and logs it; sync_run knows which peers it found. None of it survives the call.
This is the mechanism behind #682: a daemon ran 102 consecutive passes importing nothing, logging Pulling... each time. Not that nobody looked — there was nothing to look at.
Proposal
Return a SyncReport from sync_run, aggregate it in pull_all/push, and persist the latest one:
pub struct SyncReport {
pub started: DateTime<Utc>,
pub finished: DateTime<Utc>,
pub mode: SyncMode,
pub peers: Vec<PeerReport>, // one per peer considered
pub pushed: Vec<BucketReport>,
}
pub struct PeerReport {
pub device_id: String,
pub hostname: String,
pub path: PathBuf,
pub outcome: PeerOutcome, // Imported | Skipped(reason) | Failed(error)
pub buckets: Vec<BucketReport>,
}
pub struct BucketReport {
pub bucket_id: String,
pub events_new: i64,
pub resumed_at: Option<DateTime<Utc>>,
}
Then:
Scope boundary
This covers facts about what a pass did. It deliberately does not cover facts the sync folder cannot currently express — "which device is this peer", "when did it last push", "is a peer missing that used to be here". Those need per-device metadata in the folder (the manifest discussed in ActivityWatch/activitywatch#302 / #691) and are out of scope here.
Worth doing first precisely because it is small, has no format implications, and unblocks three other issues.
Credit: found in a design review of aw-sync.
Related: #684, #687, #688, #682, ActivityWatch/aw-android#274.
cc @TimeToBuildBob
A sync pass is modelled as a side effect rather than as a function that returns what it did. That single choice is why sync failures are structurally invisible, and it is cheap to reverse.
Today
sync_run(...) -> Result<(), Box<dyn Error>>— success carries no information.The JNI wrapper reports a fixed string regardless of what happened:
Ten peers or zero peers, a million events or none — identical output.
Android persists exactly
SyncStatus(completedAt: Long, success: Boolean)(Sync status is a timestamp and a boolean — the model cannot answer what synced, when next, or from whom aw-android#274).The daemon has no status at all; its only signal is exiting (aw-sync: per-peer and per-bucket errors abort the whole sync pass (and burn the supervisor's restart budget) #688).
The information mostly already exists and is discarded.
sync_onecomputesnew_events_countper bucket and logs it;sync_runknows which peers it found. None of it survives the call.This is the mechanism behind #682: a daemon ran 102 consecutive passes importing nothing, logging
Pulling...each time. Not that nobody looked — there was nothing to look at.Proposal
Return a
SyncReportfromsync_run, aggregate it inpull_all/push, and persist the latest one:Then:
aw-sync status(feat(aw-sync): addstatusdoctor command and fail-loud empty-pull warnings #687) reports the last pass as well as current folder state.PeerOutcome::Skippedgives the duplicate-device_id dedupe from fix(aw-sync): skip duplicate device_id folders that truncate history on pull #686 somewhere to report itself, instead of a log line nobody reads.Scope boundary
This covers facts about what a pass did. It deliberately does not cover facts the sync folder cannot currently express — "which device is this peer", "when did it last push", "is a peer missing that used to be here". Those need per-device metadata in the folder (the manifest discussed in ActivityWatch/activitywatch#302 / #691) and are out of scope here.
Worth doing first precisely because it is small, has no format implications, and unblocks three other issues.
Credit: found in a design review of aw-sync.
Related: #684, #687, #688, #682, ActivityWatch/aw-android#274.
cc @TimeToBuildBob