docs/predictive-refresh-policy.md
CodexBar may offer an opt-in Adaptive refresh frequency that adjusts the existing provider-batch timer between 2 and
30 minutes using the deterministic policy below. Do not implement the broader
per-account prediction, persistent interaction history, learned ranking, or menu prewarming proposed in the original
RFC.
This approval covers the bounded design only. Runtime implementation, tests, localization, and packaged proof remain a separate change.
| Option | Freshness | Complexity | Provider work | Decision |
|---|---|---|---|---|
| Keep fixed frequencies only | Predictable | Lowest | Predictable | Safe fallback |
| Add bounded adaptive batch cadence | Better while active; quieter while idle | Small | Bounded | Recommended experiment |
| Add per-provider/account prediction | Potentially best | High | Harder to reason about | Reject for now |
| Add learned ranking or contextual bandits | Unproven | Very high | Harder to audit | Reject |
The recommended option is intentionally less ambitious than “predictive refresh.” It solves the scheduling question without creating a second account model, persistent behavioral telemetry, or a new menu-rendering architecture.
Current main has two independent refresh paths:
UsageStore.startTimer() reads SettingsStore.refreshFrequency.seconds, sleeps for the fixed interval, then calls
UsageStore.refresh(). A refresh is one concurrent batch over enabledProvidersForBackgroundWork().StatusItemController.scheduleOpenMenuRefresh(for:) retries rendered providers with missing/stale data. When the
default-off Refresh when the menu opens setting is enabled, it refreshes every enabled provider instead. Both
modes use the background interaction context, coalesce in-flight provider work, and keep prompt-capable OpenAI
dashboard work deferred until menu tracking ends.Relevant implementation seams:
Sources/CodexBar/SettingsStore.swift: RefreshFrequency and fixed interval mapping.Sources/CodexBar/UsageStore.swift: timer ownership and provider-batch refresh.Sources/CodexBar/UsageStore+Refresh.swift: provider refresh coalescing and result application.Sources/CodexBar/StatusItemController+Menu.swift: missing/error-only menu-open refresh.Sources/CodexBar/StatusItemController+MenuInteractionRefresh.swift: deferred non-interactive refresh safety.Tests/CodexBarTests/StatusMenuInstantOpenTests.swift: fresh, missing, in-flight, and close-during-refresh contracts.The adaptive experiment must change only the first path. It must not alter the menu-open setting, its default, provider selection, interaction context, or promise that menu-open refresh does not reset the periodic refresh clock.
Adaptive as a mutually exclusive RefreshFrequency choice.5 minutes as the default for new and existing users.Adaptive.Manual and every existing fixed interval exactly.Use a pure AdaptiveRefreshPolicy that returns the delay before the next ordinary provider-batch refresh.
struct AdaptiveRefreshPolicy: Sendable {
struct Input: Sendable, Equatable {
let now: Date
let lastMenuOpenAt: Date?
let lowPowerModeEnabled: Bool
let thermalState: ProcessInfo.ThermalState
}
enum Reason: String, Sendable {
case recentInteraction
case warm
case idle
case longIdle
case constrained
}
struct Decision: Sendable, Equatable {
let delay: Duration
let reason: Reason
}
func nextDelay(for input: Input) -> Decision
}
Policy table, evaluated after startup and after every completed or skipped timer tick:
| Condition, first match wins | Next delay | Reason |
|---|---|---|
| Low Power Mode or serious/critical thermal state | 30 minutes | constrained |
| Menu opened at or after 5 minutes ago, including a future clock-adjusted timestamp | 2 minutes | recentInteraction |
| Menu opened more than 5 minutes and at most 1 hour ago | 5 minutes | warm |
| Menu opened more than 1 hour and less than 4 hours ago | 15 minutes | idle |
| No menu open recorded, or last open at least 4 hours ago | 30 minutes | longIdle |
Bounds are part of the contract:
The policy deliberately excludes quota level, provider latency, error count, account choice, time-of-day, and content change rate. Those signals require new durable state or provider-specific semantics and do not belong in the first experiment.
Keep scheduling inside UsageStore; do not add a second scheduler abstraction.
RefreshFrequency with adaptive; its fixed seconds value remains nil.startTimer() loop with a loop that asks a small helper for the next delay:
AdaptiveRefreshPolicy.refresh() batch path.UsageStore or a dedicated in-memory signal object. Do
not couple the policy to NSMenu, menu descriptors, account switchers, or rendering state.NSBackgroundActivityScheduler is out of scope. Current refresh choices include intervals below the range where that
API is intended to help, and using two scheduling mechanisms would make cancellation and exact timing harder to audit.
Revisit it only with separate energy measurements and a design for launch/relaunch behavior.
The first experiment stores no interaction history.
lastMenuOpenAt in memory; reset it on launch.Reason code through the existing local logger.This avoids a new retention policy, migration, deletion UI, and behavioral profile. Persistent history requires a new privacy and storage decision; it is not an incremental extension of this proposal.
Adaptive scheduling controls only when the existing batch is requested. All provider behavior remains downstream:
Do not add a scheduler-level failure backoff in the first experiment. Providers have different failure semantics, and the current failure gates primarily control error publication rather than retry eligibility. A shared backoff would need a separate contract for partial batch success.
Each step should be independently reviewable:
Adaptive setting and localization, default unchanged.scheduleOpenMenuRefresh(for:).Do not add target adapters, outcome databases, account/workspace prediction, learned models, visible ordering changes, or menu prewarming as part of these steps.
UsageStore.refresh();Use stubs and test stores. Do not run live providers, browser-cookie imports, or Keychain reads for policy validation.
Before changing the default, a separate proposal must provide measured evidence. Minimum evidence:
Rollback is deleting the Adaptive option and policy helper. Fixed/manual scheduling and stored fixed selections remain
valid because the experiment does not migrate them or change their raw values.
Approval of this document does not approve:
NSBackgroundActivityScheduler adoption;Any of those requires its own evidence and product/privacy review.