Skip to content

aw-notify: achievements / streaks / trophies (split from #200) #205

Open

Description

@TimeToBuildBob

Splitting this out from #200, per @ErikBjare's suggestion — the achievements/streaks/trophies direction is a distinct enough feature to track on its own rather than living inside the broader "smarter-than-thresholds" notifications thread.

Background

In #200 we compared anomaly-detection notifications (percentile/sigma above baseline) against a "coaching"/reward angle, and landed on achievements as a promising first step:

  • Works with minimal history (a first occurrence is still a valid achievement) — unlike anomaly detection, which needs ~30 days of baseline before it's useful.
  • Framing is additive/positive rather than corrective.
  • "Trophy for best focus day this month" is more shareable/engaging than "you're 2σ above baseline."
  • Fits the existing notification infrastructure (per-category historical time from aw-server) either way.

References:

Open design questions (from @ErikBjare, carried over from #200)

  • Storage: how do achievements get stored once conditions are met? Need references to which data sources were used, the time-range covered, and the source devices/buckets involved — not just a flat "achievement unlocked" flag.
  • Multi-device: should each device have its own achievement store that overlays locally, or should achievements sync across devices?
    • Sync as part of "settings"? Is settings-sync even supported yet?
    • Or should achievements live in their own dedicated bucket (e.g. an achievements bucket)? Is that a good fit for the existing bucket/event model, or a bad one?
  • Trophies should be collectable multiple times (e.g. per time-period — "best focus day this month" can recur), not one-shot unlocks.

Scope (not yet decided)

This issue is for continuing the design discussion — no implementation approach is locked in yet. Candidate shape: define achievement conditions declaratively, evaluate them against aw-server query results, surface via aw-notify, with the storage/sync model above still open.

cc @ErikBjare

Activity

  1. ErikBjare commented on Jul 25, 2026

    @ErikBjare
    Member

    Which other design questions exist? Make a draft design that we can start iterating on.

    Perhaps do/plan a wider literature search than that one single paper.

  2. TimeToBuildBob commented on Jul 25, 2026

    @TimeToBuildBob
    ContributorAuthor

    Thanks for the direction, Erik. Here's a draft design plus additional open questions and a broader literature sweep.


    Wider literature

    Beyond the ACM CHI paper already linked, relevant work:

    • Deterding et al. (2011) — "Gamification: Using game-design elements in non-gaming contexts" (CHI '11) — the canonical taxonomy (points/badges/leaderboards). Still the right baseline for vocabulary.
    • Trophies, achievements, and badges: SDT + Hexad player-type (2026, ScienceDirect) (link) — most directly relevant: explains why trophies motivate differently from badges/points, grounded in Self-Determination Theory. Key finding: trophies work best for "achiever" and "philanthropist" player types; streaks work best for "free spirit." Personalisation matters.
    • Gamification for health promotion — systematic review (PMC) (link) — meta-analysis of 64 gamified health apps. Self-monitoring (86%) + rewards/threat (81%) + goals/planning (81%) are the dominant mechanics. Behaviour change requires at least self-monitoring + feedback loop — not just a trophy gallery.
    • Motivation + Personality in Gamified Walking App (CHI 2026) (link) — empirical study on gamified physical activity app; good parallel to focus-tracking.
    • Trophy.so blog — "Productivity app gamification that doesn't backfire" (link) — practitioner summary of the failure modes: rewarding motion not output, badge fatigue after 1 week, streak-break abandonment. Worth skimming for design anti-patterns.

    Key synthesis from the literature: Streaks drive retention but also cause "quit after break" abandonment (high variance). Trophies are lower-pressure but need genuine rarity to stay meaningful. The most robust pattern is a mix: a few streaks (habit-building) + periodic "personal best" trophies (positive reinforcement, no failure state).


    Additional open design questions

    Beyond the storage/sync/recurrence questions already in the issue:

    Evaluation timing

    • When does the evaluation run? Options: (a) on-demand when opening the app, (b) daily scheduled worker (most natural for "day" windows), (c) event-triggered after every sync. On Android, background execution restrictions mean (b) probably routes through the existing aw-notify worker schedule, not a new service.
    • Time zone: "best day" must anchor to local midnight, not UTC. The worker needs to know the device's TZ at evaluation time.

    Notification deduplication

    • "Best focus day this month" could be true at 2pm and still be true at 8pm (you didn't lose the title). How does the worker avoid re-notifying every evaluation cycle? Proposal: store window (e.g. 2026-07) on the achievement event; skip re-notification if an event with the same achievement_id + window already exists in the bucket.

    Retroactive boot

    • On first install with 6 months of data: do we evaluate historical achievements? Risk: "You unlocked 83 achievements" is noise, not reward. Options: (a) forward-only (cleanest), (b) retroactive but capped (e.g. top-5 per category), (c) retroactive with a single "welcome trophy" summarising past bests.

    Achievement conditions: shipped vs. user-defined

    • Is this a fixed curated set (shipped in aw-notify), or can users define custom achievement conditions? Curated is simpler to ship and avoids "gaming the system." But AW's ethos is user control. A hybrid — curated defaults + optional user rules — is probably right long-term, but the v1 can be curated-only.

    Sensitive categories

    • "Longest Reddit session" is a valid data point but a weird trophy to celebrate. Should achievements filter against a "positive categories" list (user-configured or heuristic), or is all tracked time fair game?

    Cold-start for "personal best"

    • "Best focus day this month" has no baseline in month 1. Options: (a) only award once ≥7 days of data exist in the window, (b) award unconditionally ("first focus day of the month" is also a valid first-occurrence achievement), (c) use a shorter lookback window initially that grows with data age.

    Sharing / export

    • The issue mentions trophies are "shareable/engaging." What's the share surface? Plain text notification, a generated image card, a deep link to a web view? This is probably out of scope for v1 but worth noting so the storage model doesn't close it off.

    Draft design

    Achievement definition (declarative, shipped in aw-notify)

    data class AchievementDef(
        val id: String,               // "best-work-day-month"
        val title: String,            // "Best Work Day This Month"
        val category: String,         // "Work" (matches aw-server category name)
        val window: Window,           // DAY, WEEK, MONTH, ALL_TIME
        val condition: Condition,     // FIRST_OCCURRENCE | PERSONAL_BEST | STREAK_N_DAYS
        val recur: Boolean = true,    // can be awarded again in next window
    )

    A small curated set to start — maybe 10–15 definitions across Work, Exercise, Reading, etc. — avoids decision paralysis and lets us tune before exposing a user rule engine.

    Storage: events in a dedicated bucket

    Use an aw-achievements-<device-id> bucket, one event per achievement unlock:

    {
      "timestamp": "2026-07-25T17:43:00Z",
      "duration": 0,
      "data": {
        "achievement_id": "best-work-day-month",
        "title": "Best Work Day This Month",
        "value_seconds": 16200,
        "previous_best_seconds": 13680,
        "window": "2026-07",
        "data_sources": ["aw-watcher-window", "aw-watcher-afk"],
        "buckets_used": ["aw-watcher-window_phone-abc123"],
        "time_range_start": "2026-07-25T00:00:00Z",
        "time_range_end": "2026-07-25T17:43:00Z"
      }
    }

    Why bucket/events:

    • Fits the existing data model; no schema migrations
    • Append-only; history is preserved naturally
    • Queryable via the aw-server query API (load all achievements for a gallery view, filter by window for dedup)
    • Multi-device: each device gets its own bucket; an overlay query at read time gives a cross-device view
    • Sync: follows whatever aw-sync does for other buckets — no special-case needed

    Evaluation flow

    [aw-notify scheduled worker, daily]
      for each AchievementDef:
        1. Query aw-server for category time in window
        2. Check condition (personal best? streak? first occurrence?)
        3. Query achievements bucket: has this (id, window) already been awarded?
        4. If new → write event to bucket → emit notification via aw-notify
    

    The notification payload uses the existing aw-notify channel; no new Android notification infrastructure needed.

    Multi-device sync

    Phase 1: per-device buckets, no cross-device merge. Each phone awards independently. "Best focus day" can be earned on two devices in the same month if both have data — that's fine; the events are distinct by device bucket.

    Phase 2 (later): a merge/dedup step at query time when displaying an achievement gallery — show unique (id, window) pairs across all device buckets, prefer the one with the higher value.


    Happy to iterate on any of this. The two biggest open questions for the v1 scope in my view are: (a) retroactive boot policy, and (b) cold-start handling for personal-best conditions. The rest seems tractable.

  3. ErikBjare commented on Jul 26, 2026

    @ErikBjare
    Member

    Yeah I think the retroactive boot policy should apply since a "first achievement" or similar. Long-time users should not get dozens of achievements when first starting (past achievements are noise, part of the "feature" is to be notified when it happens (tie the reward/feedback), not days or weeks later).

    I'm curious how games with e.g. Steam Achievements handle stuff like this (and to which extent Steam handles it for them) and are designed generally.

    We should definitely support user-defined achievements eventually (could be shared and imported), but could also just accept PRs for new achievements ideas (although slow release cycle, might want to let people self-config then upstream).

    Keep working on the design/spec.

  4. TimeToBuildBob commented on Jul 26, 2026

    @TimeToBuildBob
    ContributorAuthor

    Good points. Let me address each.


    Steam achievement design — what they do and what's worth borrowing

    Steam handles the platform layer (display, social proof, unlock notification overlay) while games own the definition and evaluation logic. Key patterns:

    Declarative definitions — achievements are defined in Steamworks with: id, display name, description, locked/unlocked icons, a hidden flag (suppresses description until unlocked — good for surprise), and an optional progress stat (e.g. "kill 100 enemies" links to a kills stat, Steam renders a progress bar). The stat-linked model is worth borrowing: instead of binary "achieved/not," show progress toward the next threshold ("3/7 days Focus Marathon").

    Platform evaluates, game just calls SetAchievement(id) — the platform handles dedup and notification timing. Relevant here because it means the evaluation loop doesn't need to know about notification state; the bucket event IS the canonical unlock record, and the notification layer just watches for new events it hasn't surfaced yet.

    Retroactive by design, but games mostly opt out — Steam's API will retroactively unlock if a game defines a new achievement and the player already meets the condition. But most games don't do bulk retroactive unlocks because it cheapens the reward. The ones that work well do one thing: a single "you've been with us since launch" veteran trophy. That's the right model here too.

    Global achievement percentages — Steam shows "7.3% of players have unlocked this." It's surprisingly motivating. Out of scope for v1 (requires server-side aggregation), but worth noting: the bucket/events model supports this naturally later — an aw-server stats query across all opted-in users could generate the same signal.

    Hidden achievements — Steam shows a locked mystery trophy with no description. Works well for first-occurrence surprises ("You've tracked every day for a week" shouldn't telegraph itself in advance or it becomes a chore).


    Retroactive boot policy — updated

    Based on your direction: forward-only as the baseline. No storm of unlocks on first run.

    One exception worth keeping: a veteran trophy awarded on the first evaluation run if the account has data older than, say, 90 days. One notification. Acknowledges they've been tracking for a while without flooding them with retroactive personal bests. Games do this — "Veteran Explorer" for long-time players feels earned, not algorithmic.

    Everything else: forward-only. "Best Work Day This Month" only ever fires from the point aw-notify is running, not for July 2024 that already happened.

    Updated evaluation flow:

    [aw-notify scheduled worker, daily]
      if first_run and account_data_age > 90d:
        → award "aw-veteran" achievement (once, no recurrence)
      for each AchievementDef (excluding retroactive-boot flag):
        → evaluate condition against current window
        → dedup via bucket query (id + window already exists?)
        → if new: write event, notify
    

    User-defined achievements — agreed on the phased approach

    Curated-first makes sense. A PR-based expansion path is fine as a starting point; if community demand is high, the declarative AchievementDef structure in the draft design is already close to what a user-importable format would look like (a YAML/JSON file with the same fields). That door stays open without us having to build the editor now.

    One practical note: if we ship with ~10 curated achievements and a user wants to add "longest uninterrupted reading session," the condition logic needs to be general enough that they can express it by adding a definition, not by modifying evaluation code. The condition: PERSONAL_BEST | FIRST_OCCURRENCE | STREAK_N_DAYS enum is probably too narrow for that. A more composable predicate might be worth thinking about now even if user-defined definitions aren't exposed yet.


    Remaining open questions before spec is ready to implement

    1. Progress bar / partial progress — worth including in v1? Steam's stat-linked model is good UX ("3/7 days toward Focus Marathon"). Requires storing intermediate state somewhere (not a fit for the events bucket). Options: ephemeral in-app state (recomputed each evaluation) or a separate lightweight state store. Leaning toward recomputed-each-time for simplicity.

    2. Achievement definition format — Kotlin data class in the draft works for now, but if we want PR-contributed additions to be easy, it might be worth a companion YAML/JSON config that gets loaded at startup.

    3. Sensitive categories — still open. Any strong opinion on whether the category filter is user-configured or heuristic (e.g. exclude known social-media/entertainment categories)?

  5. ErikBjare commented on Jul 26, 2026

    @ErikBjare
    Member

    @TimeToBuildBob important to note that all this is not only for aw-android, but for the cross-platform ActivityWatch suite as a whole, with whatever hopefully somewhat unified notification system works across both Android and desktop (Qt and Tauri distributions). So stop thinking Kotlin. I might move this issue into the main ActivityWatch repo to prevent further confusion.

    Veteran trophy is good. Also some ideas for a "Went Pro" trophy (subscribed to ActivityWatch Pro for $5/mo to support development). A "9 to 5" trophy (more than 8h of work in a day). (Cute/fun/referential achievements names like that).

    BTW, on the "day boundary" question. There is already the "start of day" setting in aw-webui (04:00 by default) which should be respected. Moving across timezones presents some problems, but should be okay if we keep invariants like "a day must always be 24", a multi-day period must be a multiple of 24h (not dilated by timezone switching), etc.

    Partial progress or progress preview is interesting. I don't have ideas about how to handle the state. Neither the buckets/events store nor the settings store is a great fit for achievement data (key-value-like? more "user data" than the "config" nature of settings).

    No idea about how to deal with sensitive categories. Achievements shouldn't leak sensitive info (and be local/private by default).

  6. TimeToBuildBob commented on Jul 26, 2026

    @TimeToBuildBob
    ContributorAuthor

    You're right — I scoped the previous draft to the Android implementation instead of the ActivityWatch product. That was the wrong boundary. I also tried transferring this issue to ActivityWatch/activitywatch, but my account lacks TransferIssue permission, so that part still needs a maintainer.

    Here is the revised cross-platform spec direction.

    Product boundary

    This should be one local ActivityWatch achievements engine, with Android, Qt, Tauri, and the web UI as adapters:

    ActivityWatch data + shared settings + Pro entitlement
                        |
                achievement evaluator
              /           |            \
     unlock records   progress view   unlock signal
                                          |
                      Android / desktop notification adapters
    

    aw-notify is an output transport, not the source of truth and not where the domain model should live. Likewise, no Kotlin-specific achievement definitions. The evaluator and schema must be portable across distributions; each shell only handles scheduling and native presentation.

    Proposed v1 contract

    Definitions are data, not platform code. A definition needs at least:

    {
      "id": "nine-to-five",
      "version": 1,
      "title": "9 to 5",
      "description": "Track more than 8 hours in one ActivityWatch day",
      "metric": { "type": "tracked_duration" },
      "condition": { "op": "gte", "seconds": 28800 },
      "period": "day",
      "repeat": "per_period",
      "visibility": "public-safe"
    }

    Keep v1's predicate vocabulary intentionally small: tracked duration, data age, consecutive qualifying periods, personal best, and an explicit external entitlement signal. Do not embed arbitrary query code in importable definitions. We can add a safe compositional DSL only after curated definitions prove which primitives are actually needed.

    Candidate curated set:

    • ActivityWatch Veteran — first evaluator run with data older than the chosen age threshold; the only retroactive unlock.
    • 9 to 5 — >8h tracked in one ActivityWatch day.
    • Went Pro — observed transition to an active ActivityWatch Pro entitlement. This should be triggered by the entitlement source, not inferred from activity data, and must not encode plan/payment details in the notification.
    • One repeatable personal-best trophy, e.g. New Personal Best for a completed week or month.
    • One streak trophy, with a deliberately low initial threshold; avoid punitive “broken streak” messaging.

    Names should be cute/referential. Conditions and notifications should stay honest: “tracked for 8h” is not automatically “worked for 8h.”

    Time windows: fixed-duration ActivityWatch days

    Use the existing startOfDay setting (04:00 default). To preserve the invariant Erik described, define an achievement day by an anchor instant plus exactly 24 elapsed hours, not “next local wall-clock 04:00.” A week is 7 × 24h and similar multi-day periods are integer multiples of 24h.

    At period creation, persist:

    • start_of_day used,
    • timezone identifier and UTC offset used to resolve the anchor,
    • period_start and period_end as UTC instants,
    • a time_policy_version.

    Timezone or DST changes do not resize an existing period; they affect only the next period anchor. This avoids 23/25-hour days and makes unlock provenance reproducible. Changing startOfDay also takes effect on the next period rather than rewriting history.

    Storage: neither raw event bucket nor settings

    I agree the earlier “just use an achievements bucket” answer was too glib. Unlocks are durable user state with uniqueness, provenance, acknowledgement, and definition-version semantics. Activity events are append-oriented time intervals; settings are mutable config. Neither models this cleanly.

    Add a small namespaced user-data store to aw-server (conceptually achievement_unlocks plus notification delivery state), exposed through a narrow API. Example unlock record:

    {
      "unlock_id": "01J...",
      "definition_id": "nine-to-five",
      "definition_version": 1,
      "period_start": "2026-07-26T02:00:00Z",
      "period_end": "2026-07-27T02:00:00Z",
      "unlocked_at": "2026-07-26T17:14:00Z",
      "value": 28817,
      "source_refs": [
        { "bucket_id": "aw-watcher-window_host", "device_id": "..." }
      ],
      "time_policy": {
        "version": 1,
        "start_of_day": "04:00",
        "timezone": "Europe/Stockholm",
        "utc_offset_minutes": 120
      },
      "privacy": "public-safe"
    }

    The uniqueness key is (definition_id, definition_version, repeat_key), where repeat_key is once or the canonical period start. Evaluation is idempotent.

    Do not store raw titles, URLs, category names, or query result rows in unlock records. source_refs should identify buckets/devices and time bounds, while the numeric evidence is the minimum aggregate needed to explain the unlock. That gives auditability without creating a second sensitive activity log.

    This new store is intentionally a prerequisite for sync semantics. Current aw-sync copies buckets, while server settings are local key/value state; silently pretending either gives us cross-device achievement sync would be wrong.

    Multi-device semantics

    An achievement belongs to the user dataset, not the evaluator device. Evaluation must query all locally available/synced source buckets and emit one logical unlock per repeat key. Source device IDs are provenance, not ownership.

    Until user-data sync exists:

    • a single server can aggregate every bucket it can see;
    • separate unsynced servers may independently unlock the same trophy;
    • gallery overlay may deduplicate on the logical uniqueness key, but must not pick a “higher” duplicate and discard provenance;
    • exactly-once behavior across disconnected servers is explicitly not promised in v1.

    When sync arrives, merge unlocks as a grow-only set by logical uniqueness key, union source refs, preserve the earliest unlocked_at, and keep delivery acknowledgements device-local so syncing an old unlock does not cause a notification storm.

    Forward-only bootstrap and notification timing

    On first enable:

    1. persist evaluation_started_at;
    2. optionally award ActivityWatch Veteran once;
    3. initialize current metric baselines without unlocking other historical achievements;
    4. only unlock when a condition crosses after evaluation_started_at.

    Definitions added later follow the same rule unless explicitly marked retroactive: veteran_only. An unlock record and notification delivery are separate: commit the unlock first, then each platform adapter may claim/display it. This makes retries safe and keeps one failed native notification from losing the trophy.

    Progress without mutable progress state

    Do not persist partial progress in v1. Expose a pure preview endpoint/result:

    preview(definition_id, now) -> current, target, period, eligible
    

    The gallery recomputes 3/7 days, 6h 42m / 8h, etc. from source data. Only unlocks are durable. If profiling later shows this is too expensive, add a disposable cache keyed by definition version + data watermark; do not turn derived progress into canonical user data.

    Privacy rules

    • Local/private by default; no global percentages or telemetry.
    • Native notifications are lock-screen surfaces, so default text must be generic and public-safe (e.g. “Achievement unlocked — 9 to 5”), with details behind an opt-in “show details in notifications” setting.
    • Curated v1 definitions should use only global tracked duration, streaks, personal bests, data age, and explicit product entitlement — not arbitrary category names.
    • User-defined category achievements can come later, defaulting to private titles and generic notification copy.
    • Sharing is explicit export of a redacted achievement card, never automatic.

    Acceptance criteria for the first implementable slice

    1. The same definition fixtures and evaluation conformance tests run against the portable engine independent of Android/Qt/Tauri.
    2. startOfDay is respected, every day is exactly 24h, and DST/timezone/start-setting changes have deterministic tests.
    3. Fresh enablement emits no historical burst; only Veteran may unlock retroactively.
    4. Re-running evaluation is idempotent; concurrent evaluators produce one logical unlock.
    5. Progress is recomputed, not stored as canonical state.
    6. Default notification payloads contain no activity/category details.
    7. An unlock records aggregate evidence and source bucket/device/time provenance without raw event content.
    8. Android and one desktop shell can consume the same unlock contract through adapters.

    Explicitly not v1

    • an arbitrary expression language or achievement editor,
    • imported community packs (keep the format forward-compatible, but start curated/PR-based),
    • public rarity/global percentages,
    • social leaderboards,
    • a claim of exactly-once unlocks across disconnected unsynced servers,
    • “streak lost” nags.

    The unresolved architectural decision is now narrow: where the portable evaluator runs and what the aw-server user-data API should look like. I would prototype the schema + pure evaluator + conformance fixtures first, then wire one desktop and one Android adapter. That tests the cross-platform boundary before committing to a daemon placement.

  7. ErikBjare commented on Jul 26, 2026

    @ErikBjare
    Member

    @TimeToBuildBob you changed the "9-to-5" meaning to "Track more than 8 hours in one ActivityWatch day" from "Track more than 8 hours of work in one ActivityWatch day". And "ActivityWatch day" sounds like some specific definition of "day" when its really just "24h period".

    Most meaningful achievements are tied to specific categories (meeting work goals, avoiding distractions), not generic "total time exceeded number".

  8. TimeToBuildBob commented on Jul 26, 2026

    @TimeToBuildBob
    ContributorAuthor

    You're right on all three counts.

    "9 to 5" regression — I dropped "work" when rewriting the portable spec because I was moving away from Kotlin, but that was the wrong call. The condition should be 8h of tracked time in the user's Work category for a day, not total tracked duration. The correct fix is to allow definitions to reference named category roles:

    {
      "id": "nine-to-five",
      "title": "9 to 5",
      "description": "Track more than 8 hours of work in a day",
      "metric": { "type": "category_duration", "category_role": "work" },
      "condition": { "op": "gte", "seconds": 28800 },
      "period": "day",
      "repeat": "per_period"
    }

    category_role is resolved at evaluation time against the user's aw-webui category configuration. That keeps the definition portable without hard-coding a category name, and without degenerating to generic total-time.

    "ActivityWatch day" jargon — dropped. "Day" means the 24h period starting at the configured start-of-day offset. No special term needed.

    Category-specific achievements being more meaningful — agreed. The curated set should lean heavily on category-specific conditions. A better v1 candidate list:

    • 9 to 5 (category_role: work, ≥8h in a day) — personal milestone, not a generic clock
    • Deep Focus (category_role: work, longest uninterrupted work window in a day ≥ some threshold) — about session quality, not just accumulation
    • Clean Day (category_role: distractions, <configured threshold in a day) — avoiding specific things is as meaningful as doing specific things; should default private in notifications ("you had a clean day" not "you spent less than Xm on Y")
    • ActivityWatch Veteran (data age ≥ threshold, retroactive-only) — onboarding trophy
    • Went Pro (Pro entitlement transition) — driven by entitlement signal, not activity data
    • A repeatable personal-best in the Work category (week or month window) — "New Work Record"

    This requires category_role to be a first-class concept in the definition format, mapping to user-configured categories at evaluation time. The notification body must not include the resolved category name (keeps the "Clean Day" notification generic on the lock screen); only the trophy gallery shows the full condition.

    The distraction case also surfaces a question about polarity: should "distracting" categories be explicitly tagged in aw-webui settings, or inferred? Explicit opt-in seems safer — users know what they want to reduce.

    Two new open questions this surfaces:

    1. What category_roles should be first-class in v1? work seems safe. distractions is useful but requires user opt-in configuration. Should there be a focus role distinct from work (uninterrupted sessions of any productive type)?

    2. For the Deep Focus condition (longest uninterrupted window), the evaluator needs a "switch-away gap" threshold — e.g. ignore category switches shorter than 5 minutes. Is that a definition-level parameter, or a global setting?

  9. ErikBjare commented on Jul 26, 2026

    @ErikBjare
    Member
    1. Idk, maybe? But whatever it is it should fit nicely into the default set of categories (which we could also improve).
    2. Sounds like a definition/detector function parameter

    Another idea in the same vein as "deep focus" could be a "pomodoro" achievement (more than 3x ~25min (say 15-35min of >80% work) work sessions a day, with tiers for number of pomodoros?)

  10. ErikBjare commented on Jul 26, 2026

    @ErikBjare
    Member

    @TimeToBuildBob see my prev comment

  11. TimeToBuildBob commented on Jul 26, 2026

    @TimeToBuildBob
    ContributorAuthor

    Pomodoro achievement is a good addition — session quality, not just accumulation, which is exactly what makes it more meaningful than a raw duration trophy.

    Pomodoro definition sketch:

    {
      "id": "pomodoro-day",
      "title": "Pomodoro Day",
      "description": "Complete 3 or more focused work sessions in a day",
      "metric": {
        "type": "qualifying_session_count",
        "category_role": "work",
        "session_min_seconds": 900,
        "session_max_seconds": 2100,
        "work_purity_threshold": 0.8,
        "gap_merge_seconds": 120
      },
      "condition": { "op": "gte", "count": 3 },
      "period": "day",
      "repeat": "per_period",
      "tiers": [
        { "suffix": "3", "threshold": 3 },
        { "suffix": "5", "threshold": 5 },
        { "suffix": "8", "threshold": 8 }
      ]
    }

    gap_merge_seconds is the definition-level parameter for merging brief interruptions — a 90-second distraction shouldn't break a pomodoro session. The work_purity_threshold (0.8 = 80%) is also definition-level, so it can be tuned or overridden without touching evaluation code.

    Tiers share the same detection logic but different threshold values and optionally a different display name suffix. They're separate unlock events, so a day with 5 pomodoros triggers both the 3- and 5-tier unlocks (in order, during the same evaluation cycle). I'd avoid a cute per-tier rename ("Pomodoro Sprint", "Pomodoro Marathon") unless Erik wants those — fine either way, just didn't want to invent names without direction.

    On category roles matching the default AW category set:

    Agreed — v1 category_role values should map directly to the default AW category tree (Work, Comms, Media, etc.), not invent new role names. work → the user's Work category subtree, distractions → an explicit user-opt-in list. If focus as a role distinct from work ever becomes useful, it can be added when the Deep Focus condition needs it, but it's not required for the Pomodoro or 9-to-5 definitions.

    Updated curated v1 set:

    • ActivityWatch Veteran (retroactive, onboarding)
    • 9 to 5 (category_role: work, ≥8h in a day)
    • Pomodoro Day / 5 / 8 (category_role: work, 3/5/8 × 15–35min sessions with ≥80% work in a day)
    • Deep Focus (category_role: work, longest uninterrupted session ≥ threshold in a day — gap parameter is per-definition)
    • Clean Day (category_role: distractions, opt-in, below threshold in a day — private notification by default)
    • New Work Record (personal best in Work category, week or month window)
    • Went Pro (entitlement signal, not activity data)

    That's 9 definitions (counting Pomodoro tiers as one family). Still tractable as a curated set before any user-defined or PR-contributed additions.

  12. ErikBjare commented on Jul 26, 2026

    @ErikBjare
    Member

    I'm thinking these "definitions" look a lot more like custom detector functions on computed activity state rather than trying to fit a "definition" onto every type of achievement. Should maybe be a "def f()" not a type definition/hard-coded schema - although I suppose thay would make the yaml/json export type stuff more difficult, and would make cross-server implementations difficult too (unless it was a kind of frontend/client feature of aw-webui somehow?)

    But maybe that's kinda what you did?

  13. TimeToBuildBob commented on Jul 26, 2026

    @TimeToBuildBob
    ContributorAuthor

    You're pointing at a real conflation in what I wrote. Let me separate the layers clearly.

    The detector and the stored result are different things.

    The detector — "did the user earn this achievement right now?" — IS a function on activity state. It doesn't need to be serializable. def best_focus_day_this_month(events) -> bool is the right shape. Trying to express that logic as a declarative schema is over-engineering the wrong layer.

    What does need a schema is the stored result: the earned-achievement event that gets written to a bucket and potentially synced. Something like:

    {
      "id": "best-focus-day-month",
      "earned_at": "2026-07-26T18:00:00Z",
      "period": "2026-07",
      "evidence": {"duration_s": 28800, "category": "Work", "rank": 1}
    }

    So: functions detect, schema stores. The cross-platform story is: each platform (aw-notify Python, Kotlin Android, aw-webui JS) implements the same named detector functions in its native language. They share nothing except the output format for the achievements bucket.

    unless it was a kind of frontend/client feature of aw-webui somehow?

    That's a valid option for the detection side — JS functions in the webui have access to query results too. But aw-notify is the right home for the notification trigger (it already queries aw-server and fires notifications), and moving detection to the webui means you only get achievements when you open the webui, not on a schedule. For streaks/deadlines that matters.

    Revised model:

    • Detection: native functions per platform, no shared schema needed
    • Storage: standardized achievements bucket with a defined event schema
    • Notification: aw-notify queries the bucket and fires on new entries
    • Sync: bucket-level, same as any other bucket

    Does that match what you were thinking, or should detection also be client-only (webui/Android) rather than running in aw-notify?

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

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