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:
- which instance — config section, database, settings file, logfile; and
- developer mode —
json_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:
- aw-core — the profile primitives, the datastore suffix, logfile naming. (PR below.)
- aw-core release to PyPI. Everything above is blocked on this — the downstream repos cannot even import
aw_core.profile until then.
- aw-server —
--profile, config section lookup, Settings, and splitting instance-selection from developer-mode as described above.
- aw-client —
ActivityWatchClient(profile=...), config sections, the persistqueue filename, aw-client CLI.
- 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.
- 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.
- 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?
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.
aw-serveris not a tracked repo).activitywatch-research).testingstill shares theactivitywatchroot (legacysqlite-testing.db). Isolation commitsd7f8db2..7d75747are on this PR, not a follow-up. Last Erik comment 2026-08-26 ("fix it").Ask: merge #167, #118, #652, #241. On #652, confirm rust
testingstaying on the shared root vs matching python'sactivitywatch-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: boolthat selects which ActivityWatch instance to run with a named instance profile, so that more than two instances can coexist on one machine.Today
--testingpicks 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:
aw-server/aw_server/main.pyconfigsection = "server" if not args.testing else "server-testing"aw-server/aw_server/config.pyport = "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 forclientaw-client/aw_client/client.py(RequestQueue)"-testing" if client.testing else ""in the persistqueue filenameaw-qt/aw_qt/config.pyconfig["aw-qt" if not testing else "aw-qt-testing"],config-testing.toml,server-testingaw-qt/aw_qt/main.pysuffix = "-testing" if testing else ""for the single-instance lockaw-core/aw_datastore/storages/peewee.py"-testing" if testing else ""in the db filenameaw-core/aw_datastore/storages/sqlite.pyself.sid + ("-testing" if testing else "")aw-core/aw_datastore/migration.pypeewee_type + ("-testing" if datastore.testing else "")aw-core/aw_core/log.py("testing_" if testing else "")in the logfile nameProposed design
A profile is a name identifying a parallel, fully isolated instance. Two derivations, applied everywhere:
So:
defaultsqlite.v1.db[server]aw-server_<ts>.logtestingsqlite-testing.v1.db[server-testing]aw-server_testing_<ts>.logresearchsqlite-research.v1.db[server-research]aw-server_research_<ts>.logThe first two rows are exactly what exists today — the legacy layout is the special case where
defaultmaps 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
--testingstays, as an alias for--profile testing.testing=True/Falsestays working in every Python API, resolving to thetesting/defaultprofiles.profilewins and a warning is logged — a legacy alias must never silently redirect an explicitly requested profile to a different datastore.AbstractStorage.testingis 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 —Researchandresearchwould otherwise be two config sections sharing one file on a case-insensitive filesystem.testingis overloaded — the profile must not inherit all of itWorth calling out explicitly, because it is the main design subtlety. In aw-server,
testingcurrently means two different things at once:json_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
researchprofile 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 onprofile == "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:aw_core.profileuntil then.--profile, config section lookup,Settings, and splitting instance-selection from developer-mode as described above.ActivityWatchClient(profile=...), config sections, the persistqueue filename,aw-clientCLI.--profile,aw-qt-<profile>config section, single-instance lock suffix, and passing--profilethrough to the modules it spawns. Needs 3 and 4 released first, since it spawns them.aw_cli, lives in the aw-core repo) —--profilefor log lookup and for theqtsubcommand. Deliberately not done in step 1: theqtsubcommand forwards the flag to aw-qt, so it is blocked on step 5.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:--testingflag, plusif !testing && cfg!(debug_assertions) { testing = true; }config.rs: a globalTESTINGstatic withset_testing/is_testing,testing: boolonAWConfig(serde(skip)), andget_config_pathchoosingconfig.tomlvsconfig-testing.tomldirs.rs:db_path(testing)choosingsqlite.dbvssqlite-testing.dbSame suffix rule applies cleanly (
sqlite-research.db,config-research.toml). The global mutableTESTINGstatic 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
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 thatdefaultresolves to the legacy unsuffixed path,testingto the legacy-testingpath, and a custom profile to its own.Does this direction look right before I take it up the stack?