Skip to content

Harden time-limit counting against batched Screen Time threshold events#18

Merged
brendan-ch merged 14 commits into
mainfrom
feat/time-limit-counting-hardening
Jun 20, 2026
Merged

Harden time-limit counting against batched Screen Time threshold events#18
brendan-ch merged 14 commits into
mainfrom
feat/time-limit-counting-hardening

Conversation

@brendan-ch

@brendan-ch brendan-ch commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Hardens time-limit enforcement against unreliable Screen Time threshold
events, which iOS batches/coalesces and re-delivers across midnight (the daily
activity uses repeats: true, so an event carries minutes-k but no date).
This caused two failure modes: phantom blocks (yesterday's spent budget
flushed the next morning re-blocked apps the user never opened today) and
under-counts (yesterday's residual events cut into today's budget), plus the
known "Usage counter stalls at ~14/15m" display lag.

Two complementary parts (design + plan in Docs/Agents/):

Part A — background hardening (unit-tested)

  • Record only for eligible ruleshandleUsageMinutes now records usage
    after the enabled/kind/un-paused/scheduled-today guards, so a stale or
    off-day event can't corrupt today's ledger.
  • Confirmed day-start gate — new DayStartStore (app group) records each
    rule's confirmed daily-activity start; checkpoints that arrive before today's
    start is confirmed are dropped (closes the pre-boundary cross-midnight race).
    Day start zeroes today's ledger once, only on the new-day transition.
  • Foreground safety netRuleEnforcer.refresh establishes today's
    confirmed start if the monitor's intervalDidStart was skipped, so the gate
    can't silently suppress a whole day's recording.

Part B — foreground authoritative reconciliation

  • Single block eventMonitoringPlan registers one minutes-<budget>
    event instead of a per-minute chain, shrinking the cross-midnight stale
    surface to its minimum.
  • Authoritative usageRuleUsage gains authoritativeMinutesUsed /
    authoritativeAsOf and effectiveMinutesUsed(asOf:); limitReached and the
    Usage strings prefer a fresh authoritative total, else fall back to the
    threshold count. One resolver does the right thing in both contexts
    (foreground trusts the report; background falls back to the event).
  • OpenAppLockReport — a new DeviceActivityReport extension (added to
    project.pbxproj) sums each rule's true daily totalActivityDuration while
    the app is foreground and writes it to the ledger; MainView hosts an
    invisible DeviceActivityReport so it recomputes whenever the app is open.

Net effect: a false block is prevented in the common case (A) or corrected
within one foreground refresh (B); the display lag is fixed.

Testing

  • Unit: 253/253 pass via the Xcode MCP on the simulator (227 baseline + 26
    new across DayStartStore, LimitEnforcement, MonitoringPlan, RuleUsage,
    UsageLedger, RuleStatus/status, RuleEnforcer, and UsageDisplay). Each
    change was written red→green.
  • Build: app + all four extensions (incl. the new OpenAppLockReport)
    build clean.

Device-only — pending verification (Simulator delivers no DeviceActivity data)

  • Usage counter shows the true total on app open (no ~14/15m stall).
  • A maxed-out day does not re-block unused apps the next morning (or
    clears within one foreground refresh).
  • A modest prior day does not shrink today's budget.
  • The single block event still blocks at the budget in pure background.
  • Report attribution covers category/web-domain selections (currently only
    application tokens are summed).
  • Tune RuleUsage.authoritativeFreshness (120s) for the foreground cadence.

Notes

  • Establishes Docs/Agents/ as an agent-modifiable docs folder (recorded in
    AGENTS.md); spec + plan live under Docs/Agents/Specs and Docs/Agents/Plans.
  • main is merged in. Since PR docs: fold the Rules feature spec into the codebase #16 folded the feature spec into source doc
    comments and deleted Docs/AGENT_RULES_FEATURE_SPEC.md, the hardening
    behavior is documented in the owning source doc comments (DayStartStore,
    LimitEnforcement, MonitoringPlan, the report files, RuleEnforcer); AGENTS.md
    gets the new components in its repo layout, "Rules feature map", and known gaps.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U

brendan-ch and others added 14 commits June 19, 2026 19:08
Add the design spec for making time-limit enforcement robust against batched
Screen Time threshold events: Part A (background hardening) and Part B
(foreground authoritative reconciliation via a DeviceActivityReport extension).

Establish Docs/Agents/ as an agent-modifiable working-docs folder (ownership by
location, no AGENT_ prefix needed inside it) and record it in AGENTS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
Bite-sized red/green TDD plan: Part A (background hardening, tasks A1-A4) then
Part B (collapse event + authoritative reconciliation, B1-B7), plus a docs/memory
update (C1). Device-only tasks (report extension, pbxproj) are build-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
Per-rule app-group store of the last confirmed daily-activity interval start,
so limit enforcement can reject usage checkpoints that arrive before today's
boundary has been observed (stale cross-midnight flushes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
Move recordMinutesUsed below the isEnabled/kind/\!paused/scheduledToday guards in
handleUsageMinutes, so a stale or off-day threshold event can no longer corrupt
today's ledger for a rule that can't be active today.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
handleDayStart now records today as the rule's confirmed daily-activity start
(zeroing today's ledger once, only on a genuine new-day transition), and
handleUsageMinutes drops any checkpoint that arrives before today's start is
confirmed. Closes the pre-boundary race where a stale cross-midnight flush would
otherwise pass the magnitude guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
RuleEnforcer.refresh establishes today's confirmed daily-activity start for
enabled time-limit rules when missing (without zeroing), so a skipped monitor
intervalDidStart can't block usage recording for the whole day — it self-heals
the next time the app is foregrounded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
Register only a single minutes-<budget> DeviceActivityEvent per time-limit rule
instead of minutes-1..N. It serves as the background block trigger; live
sub-budget progress now comes from the DeviceActivityReport extension. Shrinks
the cross-midnight stale-flush surface to a single event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
RuleUsage gains optional authoritativeMinutesUsed/authoritativeAsOf and
effectiveMinutesUsed(asOf:freshness:), which prefers a fresh authoritative
report total and otherwise falls back to the monotonic threshold count.
Optionals stay memberwise-omittable and legacy blobs decode unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
UsageLedger.recordAuthoritativeMinutes writes the report's true daily total and
its timestamp without touching the monotonic threshold count, so the foreground
can prefer it while the background block path keeps using threshold events.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
limitReached(given:at:) on BlockingRule and RuleSnapshot now compares the
effective minutes (fresh authoritative total, else threshold) against the
budget; callers in status, the LimitEnforcement handlers, and
UninstallProtectionPolicy thread now. A fresh report total clears a phantom
foreground block and blocks ahead of a lagging threshold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
usagePhrase(for:usage:asOf:) and rowContext's usedToday check now use
effectiveMinutesUsed, so the Usage section reflects the report's true daily
total when fresh — fixing the threshold-event display lag — and falls back to
the threshold count otherwise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
Add the OpenAppLockReport DeviceActivityReport extension (new app-extension
target, hand-added to project.pbxproj mirroring the existing extensions). Its
scene sums each enabled time-limit rule's true daily usage from Screen Time's
per-application totals and records it as the authoritative figure in the shared
ledger. MainView hosts an invisible DeviceActivityReport so the scene recomputes
whenever the app is foreground; the 30s refresh loop consumes the result.

Device-only: the simulator delivers no DeviceActivity data and does not render
report extensions, so this is build-verified here and pending on-device
verification (see spec section 10).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
Update spec section 5.5 (single block event, confirmed-day-start gate, the
OpenAppLockReport authoritative reconciliation and its foreground-only limit)
and the AGENTS.md known gaps with the remaining device-verification items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
Resolve conflicts from PR #16 (feature spec folded into source doc comments):
- AGENTS.md: adopt main's three-bucket Documentation structure; keep the
  Docs/Agents/ ownership-by-location note; add the OpenAppLockReport extension,
  DayStartStore, and an authoritative-usage row to the layout + feature map.
- Docs/AGENT_RULES_FEATURE_SPEC.md: accept main's deletion — the hardening
  behavior is already documented in source doc comments (DayStartStore,
  LimitEnforcement, MonitoringPlan, the report files, RuleEnforcer).
- Retarget the design spec's references from the deleted file to the new
  doc-comment convention.
RuleEnforcer.swift and RuleEnforcerTests.swift auto-merged cleanly.

Merged tree builds; 253/253 unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD4vdHbB8KqLPGYNbYNS5U
@brendan-ch brendan-ch merged commit 68bd680 into main Jun 20, 2026
2 checks passed
@brendan-ch brendan-ch deleted the feat/time-limit-counting-hardening branch June 20, 2026 01:03
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