Skip to content

TokenLink v0.3.0: local usage analytics - #8

Open
phantom5125 wants to merge 4 commits into
mainfrom
codex/v0.3.0-usage-analytics
Open

phantom5125 wants to merge 4 commits into
mainfrom
codex/v0.3.0-usage-analytics

Conversation

@phantom5125

Copy link
Copy Markdown
Owner

Summary

  • add project, model, reasoning-effort, and privacy-safe Session attribution
  • add GitHub-style activity calendar, time series, hourly distribution, and prior-period comparison
  • add estimated active time, custom 400-day date ranges, and API-equivalent pricing coverage
  • add bounded local incremental history with changed-file reparsing and unchanged-file reuse
  • version Mac and matching C152 release metadata as 0.3.0

Privacy and data semantics

  • stores normalized counters and hashed identifiers only
  • never persists prompts, replies, tool output, full paths, raw Session IDs, or raw Claude message IDs
  • labels active time and API-equivalent cost as estimates and exposes pricing coverage

Verification

  • 271 Swift tests passed; the one timing test that flaked under concurrent Universal 2 compilation passed alone and in the serial full rerun
  • Swift format strict lint passed
  • privacy scan passed
  • resource budget passed: about 9.3s, 94MB RSS, 7.8MB release executable
  • Universal 2 TokenLink-0.3.0.dmg built, mounted, code-signature checked, and checksum verified
  • C152 firmware build/simulator/contract tests and release checksums passed

Review and release boundary

This is a stacked PR on the v0.2.3 candidate so the diff contains only v0.3.0 work. Keep it open for review; do not merge. The matching v0.3.0-rc.1 GitHub prerelease will be tagged from this branch after CI is green.

@phantom5125

Copy link
Copy Markdown
Owner Author

v0.3.0-rc.2 blocker fixes are now on commit 0447072: canonical assets/branding icons, standard Contents/Resources SwiftPM loading with a mounted-DMG cold-start gate, and non-blocking C04 onboarding plus one-time firmware bond migration. Local gates passed: 271 Swift tests; strict format; privacy/resource checks; Universal 2 DMG mount/signature/icon/cold-start; C152 PlatformIO + simulator tests; firmware contract, packaging, and checksums. The branch remains unmerged.

Base automatically changed from codex/v0.2.3-costs-watchface to main August 31, 2026 14:53
@phantom5125
phantom5125 force-pushed the codex/v0.3.0-usage-analytics branch from 03a855b to 68b19cb Compare August 31, 2026 17:50
@phantom5125

Copy link
Copy Markdown
Owner Author

CI is failing — not posting LGTM.

build-test / macOS app: costDashboardAgesVisibleRowsAtTTLWithoutAnotherLoad fails (CostDashboardModelTests.swift:246). The test ages a snapshot to TTL−0.3s and asserts .healthy immediately after load; on a slow runner the load itself exceeds 0.3s, so the row is already .stale. Other tests in the file inject a clock via CostStore(now:); this one uses the real clock. Fix: inject a fixed clock (as changingDisplayPeriodUsesCachedSnapshotsWithoutReloading does) instead of relying on a wall-clock margin.

— Reviewed by unknown model

@phantom5125

Copy link
Copy Markdown
Owner Author

Review of v0.3.0 (0447072):

  1. CI is red: macOS app job fails on costDashboardAgesVisibleRowsAtTTLWithoutAnotherLoad (timing flake under concurrent Universal 2 builds), which fails the build-test gate. The PR body already acknowledges the flake — make the TTL test deterministic (inject the clock) rather than rerunning; no LGTM until checks are green.
  2. AppModel.refreshCosts(force:) calls usageAnalytics.refresh() directly, bypassing the 300s refreshTTL that loadIfNeeded() honors — every cost refresh re-parses all transcript files. Route through loadIfNeeded() unless force is true.
  3. build_release_artifact.sh cold-start gate: fixed sleep 3 + kill-by-pid is racy on slow runners, and launch_status is interpolated as ${launch_status:-0} on a path where it is never set on wait success. Prefer waiting for a readiness signal or at least documenting the 3s assumption.

Verified clean: WatchPayloadV2 omitted-vs-empty work_items matches firmware hasWorkItems semantics present since v0.2.3-rc.1, so multi-provider batches stay compatible; release.yml community/notarized split fails closed on partial secrets; GATT revision 4 bond migration is one-time and documented.

— Reviewed by Qwen

@phantom5125

Copy link
Copy Markdown
Owner Author

Not LGTM yet: CI is red on this branch.

  • build-test and macOS app fail. The failing test is costDashboardAgesVisibleRowsAtTTLWithoutAnotherLoad (CostDashboardModelTests.swift:246-247), which this PR does not modify. It ages a snapshot created at TTL - 0.3s and expects .healthy immediately after loadIfNeeded(); on a loaded runner the load itself exceeds the 0.3s margin, so the row is already .stale at the first assertion. This is a wall-clock timing flake, not a regression introduced here.
  • Suggested fix: inject a clock into CostDashboardModel/CostStore for that test (as changingDisplayPeriodUsesCachedSnapshotsWithoutReloading already does with now:) instead of relying on Task.sleep margins, then re-run CI.

The analytics design itself reads well: local-only scanning, atomic writes with 0600 permissions, hashed session IDs, bounded retention (400 days / 250k events), and file-cache reuse keyed on size+mtime. Please get CI green before merge.

— Reviewed by unknown model

@phantom5125

Copy link
Copy Markdown
Owner Author

CI is red on this PR (build-test, macOS app): costDashboardAgesVisibleRowsAtTTLWithoutAnotherLoad fails — rows are .stale where .healthy is expected (Tests/TokenLinkAppTests/CostDashboardModelTests.swift:246-247). This looks like a wall-clock TTL test racing under concurrent CI load; make it deterministic by injecting a clock instead of relying on real time. No LGTM while checks fail.

Reminder: the PR body marks this stacked PR as review-only / do-not-merge until the v0.2.3 base lands.

— Reviewed by unknown model

@phantom5125

Copy link
Copy Markdown
Owner Author

Not approving — CI is red and the PR is explicitly marked do-not-merge.

  • build-test / macOS app fail on costDashboardAgesVisibleRowsAtTTLWithoutAnotherLoad() (CostDashboardModelTests.swift:246-247), a TTL aging test. The description calls it a timing flake, but it is failing the gate now; please make it deterministic (inject a clock instead of Task.sleep) before relying on it.
  • Stacked on the v0.2.3 candidate and flagged "keep open for review; do not merge."

No unresolved comments. Will re-check once CI is green.

— Reviewed by unknown model

@phantom5125

Copy link
Copy Markdown
Owner Author

Review of v0.3.0 local usage analytics. Findings:

  1. CI is failing — do not merge until green. costDashboardAgesVisibleRowsAtTTLWithoutAnotherLoad (CostDashboardModelTests.swift:225) creates snapshots only 0.3s inside the TTL using wall-clock Date(); any CI startup latency over 0.3s makes rows stale before the first assertion. CostDashboardModel already accepts an injectable now: clock (CostDashboardModel.swift:121) — the test should use a controlled clock instead of sleeping 600ms against real time.
  2. Secret configuration changed — requesting human confirmation. release.yml now consumes HOMEBREW_TAP_TOKEN (push access to phantom5125/homebrew-tap) and Developer ID/notarization secrets, with fail-closed partial-configuration checks. The design looks right (all-or-nothing, community builds labeled ad-hoc), but per review policy this needs a human sign-off on the secret scope before merge.
  3. WatchSyncPolicy.swift — good fix: carrying the session set in exactly one packet avoids the 512-byte BLE limit that silently dropped Kimi/MiniMax payloads, with a session-only fallback packet when even the first provider overflows.
  4. UsageAnalyticsService — bounded history (400 days, 250k events, 75k/file), incremental reparse keyed on byteCount+modifiedAt, atomic 0600 writes, hashed session IDs only. Privacy semantics match the PR description.

Verdict: blocked on (1) CI and (2) human confirmation of release secrets.

— Reviewed by unknown model

@phantom5125

Copy link
Copy Markdown
Owner Author

Self-review notes (comment instead of review since GitHub blocks self-review).

CI is failing — no LGTM. build-test and macOS app failed on run 33425997417: test costDashboardAgesVisibleRowsAtTTLWithoutAnotherLoad() fails (2 issues), which fails the whole suite. That test is not added by this PR, so this branch regresses an existing CostStore TTL behavior — likely the refreshTTL: TimeInterval = 300 / fresh-dataset reuse introduced in UsageAnalyticsService interacting with the dashboard's TTL aging. Reproduce locally with bash scripts/test.sh, fix the aging path, and re-run CI before merge.

Other observations for the next push:

  1. UsageAnalyticsService.refresh() persists the full parsed event cache (up to 250k events / 400 days) to usage-analytics-v1.json with 0600 perms — good — but the atomic write has no size cap on the encoded payload; consider asserting storageBytes against a bound so a pathological transcript set can't balloon Application Support.
  2. LocalUsageIdentity.project(from:) hashes the full cwd path as project ID — fine for privacy, but note two checkouts of the same project under different roots will never merge; acceptable for a local tool, just documenting the semantics.
  3. The firmware GATT revision bump (3 → 4) intentionally clears pre-RC2 bonds once; the one-time migration guard (getUChar >= kGattRevision) looks correct and idempotent across reboots.

— Reviewed by unknown model

@phantom5125

Copy link
Copy Markdown
Owner Author

Verdict: not LGTM — CI is red and the change touches pairing security.

  1. CI failing: build-test and macOS app jobs fail on costDashboardAgesVisibleRowsAtTTLWithoutAnotherLoad (expected .healthy, got .stale). The test is not part of this diff and looks like a pre-existing timing race (it seeds snapshots at -TTL + 0.3s, so any load slower than 300 ms flips the first assertion), but a red gate blocks the release regardless — fix the flake or rerun until the cause is confirmed.
  2. Human confirmation required: kGattRevision 3→4 (firmware/stopwatch-c152/src/CodexMicroBle.cpp) wipes pre-RC2 encrypted bonds exactly once on upgrade. Please confirm the re-pairing flow on a real watch that still holds an old bond before this ships as 0.3.0.

What I verified: the new work_items presence-vs-empty contract matches the firmware (WatchModel.h replaces the set only when the key is present and leaves provider windows untouched when windows is empty), so the session-only carrier packet and old-Mac/new-firmware combinations stay compatible. Hashed project/session attribution and the 0o600 analytics store look correct.

— Reviewed by unknown model

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant