Harden time-limit counting against batched Screen Time threshold events#18
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 carriesminutes-kbut 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)
handleUsageMinutesnow records usageafter the enabled/kind/un-paused/scheduled-today guards, so a stale or
off-day event can't corrupt today's ledger.
DayStartStore(app group) records eachrule'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.
RuleEnforcer.refreshestablishes today'sconfirmed start if the monitor's
intervalDidStartwas skipped, so the gatecan't silently suppress a whole day's recording.
Part B — foreground authoritative reconciliation
MonitoringPlanregisters oneminutes-<budget>event instead of a per-minute chain, shrinking the cross-midnight stale
surface to its minimum.
RuleUsagegainsauthoritativeMinutesUsed/authoritativeAsOfandeffectiveMinutesUsed(asOf:);limitReachedand theUsage 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 toproject.pbxproj) sums each rule's true dailytotalActivityDurationwhilethe app is foreground and writes it to the ledger;
MainViewhosts aninvisible
DeviceActivityReportso 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
new across
DayStartStore,LimitEnforcement,MonitoringPlan,RuleUsage,UsageLedger,RuleStatus/status,RuleEnforcer, andUsageDisplay). Eachchange was written red→green.
OpenAppLockReport)build clean.
Device-only — pending verification (Simulator delivers no DeviceActivity data)
clears within one foreground refresh).
application tokens are summed).
RuleUsage.authoritativeFreshness(120s) for the foreground cadence.Notes
Docs/Agents/as an agent-modifiable docs folder (recorded inAGENTS.md); spec + plan live underDocs/Agents/SpecsandDocs/Agents/Plans.mainis merged in. Since PR docs: fold the Rules feature spec into the codebase #16 folded the feature spec into source doccomments and deleted
Docs/AGENT_RULES_FEATURE_SPEC.md, the hardeningbehavior 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