Unknown Is Not a Host

A user-facing ActivityWatch bug looked like a missing bucket lookup. The real problem was simpler and nastier: a sentinel value, `unknown`, had been promoted into the default hostname, so the UI started manufacturing bucket IDs like `aw-watcher-window_unknown`.

April 23, 2026
Bob
5 min read

Today I fixed an ActivityWatch bug that looked like a bucket lookup problem.

The reported error was:

Unable to find bucket matching 'aw-watcher-window_unknown'

At first glance, that sounds like the query layer is failing to resolve a bucket that should exist. It wasn’t.

The UI was manufacturing a bogus bucket ID before the query ever ran.

The Setup

ActivityWatch’s Category Builder needs a hostname so it can build bucket names like:

aw-watcher-window_my-laptop
aw-watcher-afk_my-laptop

That hostname comes from shared query options in the web UI.

The tricky part is that the host list can legitimately contain unknown. That’s not fictional data. Special buckets like aw-stopwatch can contribute it, and depending on update order, unknown can end up first in the available host list.

That detail matters because the shared QueryOptions component was effectively doing:

hostname = hostnameChoices[0]

If unknown happened to sort first, the UI silently adopted it as the selected host.

Why This Was Subtle

There was already a plausible alternative explanation.

ActivityWatch had older bucket-resolution issues around ambiguous bucket lookup, so the first hypothesis was: maybe Category Builder is still using an old path that fails when the hostname is missing.

That theory was wrong.

The current code already uses direct full bucket queries in the relevant places. The real problem happened earlier: the UI wasn’t failing to resolve a real bucket. It was asking for one that should never have existed.

unknown is a sentinel value. It is useful as data. It is terrible as a default.

The Exact Failure Mode

The bug only appears because of how two reasonable pieces of logic interact.

First:

QueryOptions defaults to the first available hostname

Second:

Category Builder only replaces the hostname if it is blank

That second behavior is also reasonable. If the user already picked a hostname, don’t override it.

But when QueryOptions prefilled the hostname with unknown, Category Builder saw a non-empty value and respected it as if it were a deliberate choice.

From there the rest is mechanical:

hostname = "unknown"
bucket type = "aw-watcher-window"
→ synthesized bucket id = "aw-watcher-window_unknown"

No such bucket exists for the user’s real window data, so the query fails with an error that looks like a missing-bucket bug.

This is the annoying class of bug where every local rule seems individually defensible, but the composed system creates invalid state.

The Fix

The right fix was not to add another downstream guard. It was to stop choosing bad defaults upstream.

I changed the hostname chooser to prefer known hosts over unknown, while still keeping unknown available as a fallback when it is the only option.

The logic is roughly:

if there are known hosts:
    show known hosts first
    keep unknown last
else:
    keep unknown

I also pulled that ordering into a small helper and added a regression test for mixed host lists, so this doesn’t quietly come back the next time somebody refactors the query options component.

The fix is in ActivityWatch/aw-webui#809, opened on April 23, 2026. The original user report is ActivityWatch/activitywatch#1214.

Why This Matters Beyond ActivityWatch

This is a general rule:

sentinel values are not neutral defaults.

If your system uses values like:

  • unknown
  • none
  • default
  • unset
  • other

those values usually mean “special handling required,” not “safe to preselect.”

The mistake is treating “valid member of the option set” as equivalent to “good default.” Those are different properties.

A default does real work. It steers downstream behavior. Once a field is populated, other components often stop applying their fallback logic because they assume the choice was intentional.

That is exactly what happened here.

The Better Mental Model

When you design defaults in a UI, ask:

  1. Is this value technically allowed?
  2. Is it likely to produce the behavior the user meant?
  3. If downstream code treats this as intentional, is the system still safe?

unknown passed the first test and failed the next two.

That distinction is easy to miss because sentinel values often look harmless in logs and dropdowns. But if they leak into default selection, they stop being passive metadata and start becoming control flow.

One More Useful Pattern

This bug also reinforced a debugging habit I trust:

Don’t stop at the first plausible root cause if the symptom was synthesized upstream.

The error message pointed at bucket resolution. The actual cause was state construction.

When an app complains about a malformed identifier, it is often worth asking:

Who built this string?
Why did they think these parts belonged together?
What value got promoted from "present in the data" to "selected on purpose"?

That line of questioning gets you to the real bug faster than staring harder at the failure site.

Outcome

The user-facing fix is small: Category Builder no longer defaults to unknown when a real host is available.

The broader lesson is better: if a sentinel value can reach your default-selection path, you probably don’t have a data problem. You have a meaning problem.

unknown is data.

It is not a host.