Skip to content

Change how hostname/devices work #302

Description

@ErikBjare

A few things we should consider changing:

  • Every device/installation should have a unique ID (a UUID) that does not change if the hostname is changed.
  • If a bucket is created with a special hostname/UUID "local" or similar then it should default to the hostname/UUID of the running instance.
    • Or it should be the responsibility of the watcher to retrieve the UUID from /api/0/info and use it during bucket creation. Although defaulting to the local value seems like better API ergonomics (and would maybe make migration easier).
    • As an example, it is required for the web watcher to work properly with sync (see Multiple web watcher buckets per instance broken due to hostname unknown #307)
  • Additionally, we might want to move the hostname (later UUID) bucket attribute into the data field of buckets. This would impose specific semantics on the data field of buckets (which we currently lack) so might need further discussion (i.e. which data attributes are reserved for internal use in aw-server?)

Originally discussed here: ActivityWatch/aw-server-rust#61 (comment)

Activity

  1. johan-bjareholt commented on Oct 4, 2019

    @johan-bjareholt
    Member

    Another thing, we might want to make buckets user-specific on platforms which have multi-user support.
    See this forum post
    https://forum.activitywatch.net/t/add-windows-username-to-bucket/374/2

  2. nilsbentlage commented on Jan 18, 2023

    @nilsbentlage

    One problem i have (and maybe its related to this):

    I have two buckets, on is [name-of-my-macbook].local and the other [name-of-my-macbook].fritz.box

    I guess the issue is the following: when i start my macbook with lan-connection plugged in, i get another host name (and also another Bucket, if i understand that right) as i get when im connected via wifi. Am i able to avoid that?

    Thx so much. You made a great tool!

  3. BelKed commented on Jan 18, 2023

    @BelKed
    Contributor

    I also had this problem and this helped me: #554 (comment)

  4. deleted a comment from stale on Oct 29, 2024
  5. deleted a comment from stale on Oct 29, 2024
  6. deleted a comment from stale on Oct 29, 2024
  7. deleted a comment from stale on Oct 29, 2024
  8. ErikBjare commented on Sep 16, 2026

    @ErikBjare
    MemberAuthor

    Some concrete evidence on where this stands, from investigating a multi-device sync failure (ActivityWatch/aw-server-rust#682, #683, ActivityWatch/aw-android#272).

    Bullet-by-bullet status in the code today

    "Every device/installation should have a unique ID" — done. aw-server/src/device_id.rs, surfaced at /api/0/info.

    "Special hostname that defaults to the local instance" — implemented, zero callers. Both servers have the !local sentinel (aw-server-rust/aw-server/src/endpoints/bucket.rs, aw-server/aw_server/api.py), and it is the only code path anywhere that writes a device ID onto a bucket:

    if bucket.hostname == "!local" {
        bucket.hostname = gethostname()...;
        bucket.data.insert("device_id".to_string(), state.device_id.clone().into());
    }

    Grepping the whole bundle for !local returns only those two server implementations — no watcher uses it. On a long-running instance of mine:

    0/32 buckets have data.device_id
    

    "Move the hostname into the data field" — not done. hostname is still a column on Bucket.

    The practical consequence

    device_id identifies the instance but never the data, so every consumer falls back to hostname. That is not abstract — it is the direct cause of a cluster of bugs we just worked through:

    One trap for whoever implements the next step

    The tempting one-liner — stamp data.device_id unconditionally in bucket_new — is wrong. AwClient::create_bucket POSTs to that same endpoint (aw-client-rust/src/lib.rs:117), so a sync pull creating an imported peer bucket would be stamped with the local device ID. That is precisely the bug class ActivityWatch/aw-server-rust#650 just fixed for $aw.sync.origin, which was being written on push-staging as well as on import and therefore meant nothing.

    A safer shape, roughly in the staging spirit of ActivityWatch/aw-server-rust#649:

    1. Keep the server stamping only on !local.
    2. Have aw-sync stamp data.device_id on imported buckets from the peer directory name it already knows — sync is the one component that always has the right answer.
    3. Migrate watchers to !local opportunistically.
    4. Only then make device_id authoritative for provenance.

    The ordering matters: make it present before making it authoritative. Right now step 4 looks like a small change and is actually a migration, because there is no data to migrate from.

    Happy to be wrong about the direction — mainly wanted the "implemented but inert" state recorded, since from the outside it looks like this bullet is done.

  9. ErikBjare commented on Sep 16, 2026

    @ErikBjare
    MemberAuthor

    Proposal: bucket identity = (device_id, id), hostname out of the ID

    This closes bullets 2 and 3 of this issue, and it is the end state the sync work in #1445 has been converging on. Erik's framing: "getting rid of hostnames in bucket-ids altogether, buckets unique on (device_id, bucket_id) instead of current bucket_id which includes hostname in id (not reliable)."

    Why now

    The sync v2 design puts every bucket at devices/{device_id}/buckets/{bucket_uid}/ with its published id in a manifest — the folder is already keyed (device_id, bucket). What stops the local server from being the same is one constraint: buckets.name TEXT UNIQUE. Because of it, an imported peer bucket has to be renamed to avoid colliding with the local one, which is the entire reason -synced-from-<origin> exists, which is the root of ActivityWatch/aw-server-rust#649, #692, #694, the hostname fork in ActivityWatch/aw-android#272, and the reason the multi-device view can't include browser buckets.

    The code is waiting for it. aw-webui/src/queries.ts:98 builds 'aw-watcher-window_' + host in the frontend and prefix-matches find_bucket("…_") when the hostname is unknown, with a comment about host vs host.local. multideviceQuery's own comments: picked in the order of the hostnames array; only supports desktop; doesn't support browser buckets due to the 'unknown' hostname. stores/buckets.ts carries // TODO: Include consideration of device_id UUID twice. And the !local sentinel — implemented on both servers, zero callers — is the watcher-side half of this, never adopted because nothing needed it.

    The model

    • Identity: (device_id, id). id is the local, hostname-free name: aw-watcher-window, aw-watcher-web-firefox, aw-stopwatch.
    • hostname is metadata on the device, not the bucket. Devices can have several over time; the current one is a display label.
    • Provenance is the device_id column. device_id != local is "derived from a peer" — no name substring, no $aw.sync.origin needed for that purpose.

    What changes, by layer

    1. Datastore (rust aw-datastore and python aw-core, in lockstep). Add buckets.device_id; UNIQUE(name) → UNIQUE(device_id, name). Migration: every existing row gets device_id = local instance id, except rows whose id carries -synced-from-<origin> → device_id from $aw.sync.origin (trustworthy since ActivityWatch/aw-server-rust#650). Do not rewrite name. Existing aw-watcher-window_erb-m2 stays aw-watcher-window_erb-m2 with device_id = local; only newly created buckets are clean. Names converge over time; the display layer strips a suffix that matches one of the device's known hostnames.

    2. REST API. Canonical: /api/0/devices/{device_id}/buckets/{id}. Alias: today's /api/0/buckets/{id} = the local device's bucket — so every existing watcher and script keeps working. A compatibility resolver for one release: an id ending in _<a hostname this device has had> resolves to (local, stripped); …-synced-from-<X> resolves to (X, stripped). New: GET /api/0/devices → [{device_id, hostnames, first_seen, last_seen, local}] — the peer catalogue the Raw Data page and sync status both need anyway.

    On the stamping hazard I raised earlier in this thread: it dissolves. Plain POST /buckets/{id} always stamps device_id = local (it must, for uniqueness); imports go through the device-scoped path and never hit it.

    3. Watchers. Send id = "aw-watcher-window", hostname = "!local". That gives !local its first callers, and it is opportunistic — an un-updated watcher sending aw-watcher-window_<host> still lands as (local, that-name).

    4. aw-query. query_bucket("aw-watcher-window") = local device by default; query_bucket("aw-watcher-window", device=<id>) for a peer; find_buckets(type=…, device=…) replaces prefix-matching find_bucket, which is what the webui means when it prefix-matches. The multi-device query becomes "for each selected device, query_bucket(id, device=d)" — no hostname array, browser buckets included because they are (device, aw-watcher-web-firefox), no unknown exclusion.

    5. aw-webui. /activity/{host}/ → /activity/{device_id}/, hostname rendered as label from /api/0/devices. queries.ts stops string-building IDs. bucketsByDevice becomes real (it already groups by device; today device_id always falls back to hostname — ActivityWatch/aw-webui#982). The "multidevice" checkbox becomes a device selector with overlap priority order.

    6. Sync v2. This is where it pays off: import writes (device_id, id) directly. No -synced-from- bucket is ever created again. ActivityWatch/aw-server-rust#694 (derived = read-only) is a device_id != local check. #692 evaporates — the hostname is not in the id. ActivityWatch/aw-server-rust#649 stage 3 is just the column.

    Sequencing — the one thing that must be decided now

    The datastore change must land before v2's import step. If v2 imports into the old model first, every imported bucket carries a suffix that then has to be migrated a second time. It can follow the manifest and segment writer, which never touch the local bucket table. So the v2 sequence in #1445 becomes:

    SyncReport (ActivityWatch/aw-server-rust#699) → manifest → segment writer → (device_id, name) identity → reader + import → derived/read-only → legacy retirement.

    API/query/webui changes can trail the datastore change — old string IDs keep resolving through the alias.

    Honest cost

    The schema change is the one non-additive step in the whole programme. Category rules, saved queries and external scripts store full bucket-ID strings — the compatibility resolver covers them for a release, and not rewriting names means nothing users see changes until they opt in. The real coordination cost is python and rust datastores moving in lockstep, since the bundle still defaults to aw-server-python; a migrated db must be readable by both. That is an argument for the bundle defaulting to rust first, or for doing the python side as a strict mirror in the same week.

    Concrete first PR (can start after the v0.14.0 cut, independent of sync): buckets.device_id column + UNIQUE(device_id, name) + migration + stamp-on-create + the /api/0/devices endpoint, in both servers, with the compatibility resolver. Additive on disk because names are untouched. Everything above builds on it.

    cc @TimeToBuildBob — this is the identity work R2 was pointing at, made explicit; it is the step to plan for right after the v0.14.0 queue clears.

  10. TimeToBuildBob commented on Sep 16, 2026

    @TimeToBuildBob
    Contributor

    Acknowledged. (device_id, id) is the end state. The 2026-08-22 staged plan (Stages 0–2, Stage 3 optional per ActivityWatch/aw-server-rust#649) is superseded: v2 import needs the datastore unique-constraint change or every imported bucket migrates twice.

    Parked until the v0.14.0 desktop cut. No PR this session.

    First PR (after the cut, independent of sync)

    Paired, same week, both servers — rust aw-datastore NEWEST_DB_VERSION 6→7 as the spec, python aw-core sqlite LATEST_VERSION 1→2 as the strict mirror. Python's identifier column is id, rust's is name; the constraint is UNIQUE(device_id, <identifier>) in both. HTTP contract must match. Do not make "bundle defaults to rust" a prerequisite of this PR.

    Scope, matching the proposal:

    1. buckets.device_id column
    2. drop single-column uniqueness on the identifier
    3. do not rewrite names
    4. stamp device_id = local on POST /api/0/buckets/{id}
    5. GET /api/0/devices
    6. compatibility resolver on the alias path

    Migration: do not put $aw.sync.origin in device_id

    $aw.sync.origin is a hostname. aw-sync/tests/sync.rs asserts it matches the source hostname; get_or_create_sync_bucket falls back to bucket.hostname. Writing it into buckets.device_id puts hostname instability back into the identity column — the ActivityWatch/aw-server-rust#683 class of bug.

    For the first PR, stamp every existing row device_id = local instance id, including -synced-from- rows. Names stay unique because the suffix is still in the id, so the new unique constraint holds. Recovering a peer UUID belongs to v2 import, which already has the peer's device_id from the folder path (the trap in the earlier comment on this issue). There is no device catalogue to join against until GET /api/0/devices exists, and that catalogue would be derived from this column — circular.

    Resolver must be bidirectional before any watcher moves

    If a watcher starts minting aw-watcher-window while aw-watcher-window_<host> holds the history, find_bucket exact-matches the new empty bucket and query-driven views lose history, while aw-webui keeps concatenating _<host> and never sees the new one.

    So the resolver needs both directions, for one release:

    • suffixed lookup → stripped row, if that's what exists
    • stripped lookup → suffixed row matching a hostname this device has had, if that's what exists

    Watcher !local / hostname-free ids stay out of the first PR. The unused !local exists-check-before-substitution bug (python api.py returns already-exists before substituting) is a prerequisite of watcher adoption, not of the column.

    Sequence (locked to #1445)

    ActivityWatch/aw-server-rust#699 → manifest → segment writer → this PR → reader+import (writes (device_id, id) directly) → ActivityWatch/aw-server-rust#694.

    Local task aw-bucket-id-device-identity-unification retargeted from "waiting on design review" to "waiting on the v0.14.0 desktop tag".

  11. TimeToBuildBob commented on Sep 17, 2026

    @TimeToBuildBob
    Contributor

    Pointer so this doesn't float free: the aw-android sync-identity fork now has an open stopgap PR — ActivityWatch/aw-android#273 (repairs the already-forked SAF folders + unsanitized buckets.hostname rows on devices that hit v0.14.2b1). It is not an alternative to the (device_id, id) work here: the schema migration below is additive (stamp device_id = local, do not rewrite names) and does not reconcile damage already on disk. Erik flagged the PR as possibly-misguided-at-942-lines; the disposition is either a trimmed stopgap merge or folding the reconciliation into this issue's first PR. Either way the first PR here is unchanged.

  12. TimeToBuildBob commented on Oct 6, 2026

    @TimeToBuildBob
    Contributor

    Phase 2.2 Python mirror PRs are now open:

    These mirror ActivityWatch/aw-server-rust#786 for the Python stack. Both PRs are paired and should be reviewed/merged together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions