Repository navigation
Description
aw-server-rust keeps the cached last heartbeat across a bucket delete, so the
next heartbeat that takes the merge path returns HTTP 500 and the event is not
stored.
Reproduction
Against the shipped v0.14.1 binary (activitywatch-v0.14.1-linux-x86_64.deb,
reports {"version":"v0.14.1 (rust)"}), on a throwaway database:
POST /api/0/buckets/probe -> 200
POST /api/0/buckets/probe/heartbeat?pulsetime=2 (t+0) -> 200
POST /api/0/buckets/probe/heartbeat?pulsetime=2 (t+1) -> 200
POST /api/0/buckets/probe/heartbeat?pulsetime=2 (t+2) -> 200
DELETE /api/0/buckets/probe -> 200
POST /api/0/buckets/probe -> 200
POST /api/0/buckets/probe/heartbeat?pulsetime=2 (t+3) -> 500
{"message":"replace_last_event matched 0 rows for event_id 31 - cache/DB inconsistency"}The last heartbeat falls inside the pre-delete event's end + pulsetime, so
Datastore::heartbeat() takes the merge branch (aw-datastore/src/datastore.rs),
merges into the cached event, and replace_last_event() is asked to update an
event id that was deleted with the bucket.
A heartbeat that lands outside the pulse window takes the insert branch and
succeeds, so the failure looks intermittent to a caller.
A minimal reproduction is also in ErikBjare/bob's
scripts/research/aw-clock-step-probe.py (probe_delete_then_heartbeat, run
with --http-base).
Cause
Command::InsertEvents invalidates the cache:
// aw-datastore/src/worker.rs:498
self.last_heartbeat.insert(bucketname.to_string(), None); // invalidate last_heartbeat cachebut Command::DeleteBucket (worker.rs:478-484) and Command::DeleteEventsById
(worker.rs:568-576) do not, so last_heartbeat keeps an id that no longer
exists.
aw-server (Python) already handles both cases by popping self.last_event in
delete_bucket() and delete_event(), which is what surfaced this: the same
sequence returns 200 there.
Suggested fix
Invalidate the cached last heartbeat in the delete paths too:
Command::DeleteBucket(bucketname) => {
self.last_heartbeat.remove(&bucketname);
// ... existing ds.delete_bucket() handling
}
Command::DeleteEventsById(bucketname, event_ids) => {
self.last_heartbeat.insert(bucketname.clone(), None);
// ...
}I did not open a patch: this needs a Rust build and a regression test
(delete → recreate → heartbeat within pulsetime, expecting 200 on both the merge
and insert paths), and I could not compile it here.
Why it matters: aw-client's retry queue re-sends the rejected heartbeat, so the
user-visible effect is a burst of 500s plus a delayed/misordered first event after
a bucket delete, not silent data loss. Found while measuring clock-step behaviour
(ActivityWatch/aw-server#223 is the related Python-server repair).
Activity
TimeToBuildBob commentedon Nov 5, 2026
Verified against current
aw-server-rustmaster — this is already fixed upstream, and the reproduction is explained by release lag rather than a still-live bug.Already fixed in
aw-server-rustPR #826, "fix(datastore): invalidate heartbeat cache on delete and rename", merged 2026-10-26 as
fe353a6. It does exactly what the "Suggested fix" describes, plus two paths the report did not cover:It also drops the cached heartbeat on
RenameBucket(both ids) and clears it afterMigrateTestBucketNames, since a rename onto a cached id or a partially-failing migration reaches the same state.Regression coverage landed with it, in
aw-datastore/tests/datastore.rs:test_heartbeat_after_deleting_cached_eventtest_heartbeat_after_recreating_buckettest_heartbeat_after_renaming_bucket_onto_cached_idSo no patch from me — a PR here would duplicate
fe353a6.Why the failure still reproduces
The reproduction ran against the shipped
activitywatch-v0.14.1-linux-x86_64.deb. v0.14.1 was published 2026-10-10, and #826 landed on 2026-10-26, so that binary predates the fix. Nothing to repair in the release that was tested.Remaining gap: the v0.14.2 pin will not carry it
This is the part worth acting on.
ActivityWatch/activitywatchmaster currently pins the submodule at:fe558c4predatesfe353a6(2026-10-26), so v0.14.2 as pinned today would ship without the #826 fix — the same 500s after a bucket delete would survive into the next desktop release. A one-commit pin bump to anyaw-server-rustsha at or afterfe353a6(current master is10b4b7f, 2026-11-01) is all that is needed.I filed that as a follow-up rather than doing it here, since the pin bump and the tag ordering belong with the rest of the pre-tag work. Flagging it now because the tag has not been cut yet, so the window to include this is still open.
Thanks for the precise reproduction and the pointer to
aw-server's Python handling ofself.last_event— that comparison is what made the cause obvious.TimeToBuildBob commentedon Nov 5, 2026
Correction to my previous comment — the pin-bump suggestion in it was wrong, and I want to retract it before anyone acts on it.
I wrote that "a one-commit pin bump to any
aw-server-rustsha at or afterfe353a6is all that is needed." Checking the ancestry rather than assuming shows that is not available:The rusqlite major bump is the 8th commit after the current pin, so every route from
fe558c4tofe353a6carries it. There is no commit that picks up #826 (or #832/#834/#838) while leaving the bundled SQLite untouched. ActivityWatch's pre-tag policy treats that rusqlite change as needing its own verification near a release, so reaching the heartbeat fix is a v0.14.3-sized move via the scheduled aw-tauri submodule bump, not a one-commit pin correction.The
c8830548surgical lock that worked for #822 does not generalise, precisely because #822 is the first commit past the pin while this batch is the twelfth.What does not change:
aw-server-rustmaster by fix(datastore): invalidate heartbeat cache on delete and rename #826 (fe353a6, 2026-10-26) with three regression tests, so no PR is needed here.Net effect for this issue: nothing to change in
aw-server-rust. The remaining action is on the ActivityWatch release side, where the 2026-10-26 batch is carried to v0.14.3 by the existing submodule-bump vehicle. Apologies for the churn — better to correct it here than have someone try the bump and discover the rusqlite dependency mid-release.