Skip to content

RFC: named instance profiles — testing-root fallback in aw-core#152; remaining PRs need the same rule #1399

Description

@TimeToBuildBob

STATUS 2026-08-28 — remaining merge stack (no new ping)

Do not rebuild. Remaining work is maintainer merge plus one design confirmation on the rust testing root.

Step PR Status
1 — aw-core dirs aw-core#149 merged 2026-08-20
6 — aw-cli aw-core#151 merged 2026-08-25
5 — aw-qt aw-qt#128 merged 2026-08-25
3 — aw-server aw-server#167 OPEN, CI green. Not on the decision dashboard (aw-server is not a tracked repo).
4 — aw-client aw-client#118 OPEN, CI green. Same dashboard hole.
7 — aw-server-rust aw-server-rust#652 OPEN, CI green. Code isolates non-testing profiles via sibling appname (activitywatch-research). testing still shares the activitywatch root (legacy sqlite-testing.db). Isolation commits d7f8db2..7d75747 are on this PR, not a follow-up. Last Erik comment 2026-08-26 ("fix it").
aw-tauri aw-tauri#241 OPEN, CI green.

Ask: merge #167, #118, #652, #241. On #652, confirm rust testing staying on the shared root vs matching python's activitywatch-testing. After #652, do not expect a second root-isolation PR. Python runtime isolation still needs an aw-core PyPI release containing #149 (PyPI is still 0.5.17).

No new GitHub ping before 2026-09-02 unless you reply.


Summary

Replace the two-valued testing: bool that selects which ActivityWatch instance to run with a named instance profile, so that more than two instances can coexist on one machine.

Today --testing picks between exactly two instances. The profile name would drive both the config section and the datastore/logfile suffix, with "default" and "testing" resolving to precisely the paths and sections that exist today.

Motivation

A third parallel instance is genuinely needed.

A "research" build of ActivityWatch used in a study at Lund University must not share a datastore with a participant's personal ActivityWatch, or personal activity leaks into the research data. Today the only mitigation is asking participants to quit their normal ActivityWatch first — which relies on the participant, is easy to get wrong, and silently produces contaminated data when it goes wrong.

More generally, a profile mechanism lets a developer run prod + testing + research side by side, and gives a clean answer for any packaged/derived build that wants its own instance.

This is adjacent to, but distinct from, #75 ("doesn't behave well on multiuser systems"): #75 is about several users on one machine, this is about several instances for one user.

Current state

The port is already fully config-driven (it comes from the config section), while the datastore suffix is derived from the boolean rather than from config:

Location Today
aw-server/aw_server/main.py configsection = "server" if not args.testing else "server-testing"
aw-server/aw_server/config.py port = "5600" in [server], port = "5666" in [server-testing]
aw-server/aw_server/settings.py "settings.json" if not testing else "settings-testing.json"
aw-client/aw_client/client.py _config["server" if not testing else "server-testing"], same for client
aw-client/aw_client/client.py (RequestQueue) "-testing" if client.testing else "" in the persistqueue filename
aw-qt/aw_qt/config.py config["aw-qt" if not testing else "aw-qt-testing"], config-testing.toml, server-testing
aw-qt/aw_qt/main.py suffix = "-testing" if testing else "" for the single-instance lock
aw-core/aw_datastore/storages/peewee.py "-testing" if testing else "" in the db filename
aw-core/aw_datastore/storages/sqlite.py self.sid + ("-testing" if testing else "")
aw-core/aw_datastore/migration.py peewee_type + ("-testing" if datastore.testing else "")
aw-core/aw_core/log.py ("testing_" if testing else "") in the logfile name

Proposed design

A profile is a name identifying a parallel, fully isolated instance. Two derivations, applied everywhere:

suffix(profile)         = ""            if profile == "default" else "-" + profile
config_section(base, p) = base + suffix(p)

So:

profile db file config section logfile
default sqlite.v1.db [server] aw-server_<ts>.log
testing sqlite-testing.v1.db [server-testing] aw-server_testing_<ts>.log
research sqlite-research.v1.db [server-research] aw-server_research_<ts>.log

The first two rows are exactly what exists today — the legacy layout is the special case where default maps to the empty suffix. No existing database, config file or logfile is renamed, moved or orphaned, and no migration is required. That is a hard requirement of this proposal; any variant that needs to rename user databases should be rejected.

Backwards compatibility

  • --testing stays, as an alias for --profile testing.
  • testing=True/False stays working in every Python API, resolving to the testing/default profiles.
  • Where both are given and disagree, profile wins and a warning is logged — a legacy alias must never silently redirect an explicitly requested profile to a different datastore.
  • AbstractStorage.testing is kept as a property derived from .profile (with a setter), so third-party code reading or assigning it keeps working.

Validation

The profile name reaches the filesystem as part of a filename, so it is validated: lowercase ASCII alphanumerics plus - and _, max 32 chars, must start with a letter or digit. That excludes path separators, .., whitespace and empty names. Lowercase-only is deliberate — Research and research would otherwise be two config sections sharing one file on a case-insensitive filesystem.

testing is overloaded — the profile must not inherit all of it

Worth calling out explicitly, because it is the main design subtlety. In aw-server, testing currently means two different things at once:

  1. which instance — config section, database, settings file, logfile; and
  2. developer modejson_provider_class.compact = not testing, debug=testing (Flask debug), extra permissive CORS in _config_cors, and bucket deletion allowed without ?force=1 (rest.py).

Only (1) generalises. A research profile is a production instance and must not get Flask debug mode, permissive CORS, or unguarded bucket deletion. The proposal is therefore that the developer-mode behaviours stay gated on profile == "testing" specifically, not on "profile is not default". aw_core.profile.is_testing_profile() exists for exactly that, so the distinction is explicit at each call site rather than implied.

(aw-server-rust does the same thing in main.rs: if !testing && cfg!(debug_assertions) { testing = true; } — debug builds force testing mode.)

Ports

The port is already per-section, so [server-research] can set its own. Open question for arbitrary profiles: what should happen when no [server-<profile>] section exists? Options are (a) hard error telling the user to add the section, (b) inherit [server] but require --port, or (c) derive a port from the name. I lean (a) — explicit, and it cannot silently collide with the running default instance on 5600. Happy to follow your preference.

Rollout ordering

These are separate repos with separate release cadences, and aw-server/aw-client/aw-qt consume aw-core from PyPI (aw-core = "^0.5.8", currently locked at 0.5.16). So the work has to land bottom-up, one release at a time:

  1. aw-core — the profile primitives, the datastore suffix, logfile naming. (PR below.)
  2. aw-core release to PyPI. Everything above is blocked on this — the downstream repos cannot even import aw_core.profile until then.
  3. aw-server--profile, config section lookup, Settings, and splitting instance-selection from developer-mode as described above.
  4. aw-clientActivityWatchClient(profile=...), config sections, the persistqueue filename, aw-client CLI.
  5. aw-qt--profile, aw-qt-<profile> config section, single-instance lock suffix, and passing --profile through to the modules it spawns. Needs 3 and 4 released first, since it spawns them.
  6. aw-cli (aw_cli, lives in the aw-core repo) — --profile for log lookup and for the qt subcommand. Deliberately not done in step 1: the qt subcommand forwards the flag to aw-qt, so it is blocked on step 5.
  7. aw-server-rust — separate follow-up, see below.

Steps 3, 4 and 5 are each independently backwards compatible, so they don't have to ship together — an aw-qt that doesn't yet know about profiles simply keeps passing --testing.

aw-server-rust

Checked: it has the equivalent flag and would need the same treatment, in aw-server/src/:

  • main.rs: --testing flag, plus if !testing && cfg!(debug_assertions) { testing = true; }
  • config.rs: a global TESTING static with set_testing/is_testing, testing: bool on AWConfig (serde(skip)), and get_config_path choosing config.toml vs config-testing.toml
  • dirs.rs: db_path(testing) choosing sqlite.db vs sqlite-testing.db

Same suffix rule applies cleanly (sqlite-research.db, config-research.toml). The global mutable TESTING static is the awkward part and probably wants to become a profile string on the config. I'd treat this as a separate follow-up rather than blocking the Python side on it — but the two must agree on the naming scheme, which is the reason to settle the scheme here first.

Out of scope

  • Renaming or migrating any existing data. Explicitly a non-goal.
  • A UI for switching profiles.
  • Per-profile watcher management beyond passing the flag through.

First PR

ActivityWatch/aw-core#149 implements step 1 only: aw_core.profile, the datastore suffix across all three storages, migration.py, and logfile naming — all backwards compatible, with tests covering that default resolves to the legacy unsuffixed path, testing to the legacy -testing path, and a custom profile to its own.

Does this direction look right before I take it up the stack?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions