Skip to content

Commit 692da32

Browse files
hhh2210steipete
andauthored
docs: propose predictive refresh policy (#1739)
* docs: propose predictive refresh policy * Expand predictive refresh target model * docs: narrow adaptive refresh proposal * docs: accept bounded adaptive refresh Co-authored-by: hhh2210 <hzy2210@gmail.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent bcff888 commit 692da32

2 files changed

Lines changed: 246 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Changed
66
- Website: redesign codexbar.app around faster download, provider discovery, feature, CLI, and widget paths with responsive dark/light and localized layouts. Thanks @vyctorbrzezowski!
7+
- Architecture: accept a bounded opt-in adaptive refresh design with a deterministic 2–30-minute cadence and no behavioral telemetry. Thanks @hhh2210!
78

89
## 0.38.0 — 2026-07-03
910

docs/predictive-refresh-policy.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
---
2+
summary: "Decision record for an optional deterministic adaptive refresh cadence."
3+
read_when:
4+
- Planning refresh cadence or background provider updates
5+
- Evaluating adaptive or predictive refresh behavior
6+
- Changing UsageStore timer scheduling
7+
---
8+
9+
# Adaptive refresh decision record
10+
11+
- **Status:** Accepted design; not implemented
12+
- **Decision owner:** Maintainer
13+
- **Runtime impact:** None until separately implemented
14+
15+
## Decision
16+
17+
CodexBar may offer an opt-in `Adaptive` refresh frequency that adjusts the existing provider-batch timer between 2 and
18+
30 minutes using the deterministic policy below. Do not implement the broader
19+
per-account prediction, persistent interaction history, learned ranking, or menu prewarming proposed in the original
20+
RFC.
21+
22+
This approval covers the bounded design only. Runtime implementation, tests, localization, and packaged proof remain a
23+
separate change.
24+
25+
## Options considered
26+
27+
| Option | Freshness | Complexity | Provider work | Decision |
28+
|---|---|---|---|---|
29+
| Keep fixed frequencies only | Predictable | Lowest | Predictable | Safe fallback |
30+
| Add bounded adaptive batch cadence | Better while active; quieter while idle | Small | Bounded | Recommended experiment |
31+
| Add per-provider/account prediction | Potentially best | High | Harder to reason about | Reject for now |
32+
| Add learned ranking or contextual bandits | Unproven | Very high | Harder to audit | Reject |
33+
34+
The recommended option is intentionally less ambitious than “predictive refresh.” It solves the scheduling question
35+
without creating a second account model, persistent behavioral telemetry, or a new menu-rendering architecture.
36+
37+
## Current behavior and code seams
38+
39+
Current `main` has two independent refresh paths:
40+
41+
1. `UsageStore.startTimer()` reads `SettingsStore.refreshFrequency.seconds`, sleeps for the fixed interval, then calls
42+
`UsageStore.refresh()`. A refresh is one concurrent batch over `enabledProvidersForBackgroundWork()`.
43+
2. `StatusItemController.scheduleOpenMenuRefresh(for:)` retries rendered providers with missing/stale data. When the
44+
default-off **Refresh when the menu opens** setting is enabled, it refreshes every enabled provider instead. Both
45+
modes use the background interaction context, coalesce in-flight provider work, and keep prompt-capable OpenAI
46+
dashboard work deferred until menu tracking ends.
47+
48+
Relevant implementation seams:
49+
50+
- `Sources/CodexBar/SettingsStore.swift`: `RefreshFrequency` and fixed interval mapping.
51+
- `Sources/CodexBar/UsageStore.swift`: timer ownership and provider-batch refresh.
52+
- `Sources/CodexBar/UsageStore+Refresh.swift`: provider refresh coalescing and result application.
53+
- `Sources/CodexBar/StatusItemController+Menu.swift`: missing/error-only menu-open refresh.
54+
- `Sources/CodexBar/StatusItemController+MenuInteractionRefresh.swift`: deferred non-interactive refresh safety.
55+
- `Tests/CodexBarTests/StatusMenuInstantOpenTests.swift`: fresh, missing, in-flight, and close-during-refresh contracts.
56+
57+
The adaptive experiment must change only the first path. It must not alter the menu-open setting, its default, provider
58+
selection, interaction context, or promise that menu-open refresh does not reset the periodic refresh clock.
59+
60+
## Accepted product contract
61+
62+
- Add `Adaptive` as a mutually exclusive `RefreshFrequency` choice.
63+
- Keep `5 minutes` as the default for new and existing users.
64+
- Never migrate an existing fixed selection to `Adaptive`.
65+
- Preserve `Manual` and every existing fixed interval exactly.
66+
- Schedule the same enabled-provider batch as fixed refresh; do not select accounts, workspaces, or data lanes.
67+
- Keep manual refresh immediate and user-initiated.
68+
- When refresh-all-on-open is disabled, keep menu-open refresh missing/error-only and background/non-interactive.
69+
- Preserve the opt-in refresh-all-on-open path exactly. Recording a menu-open timestamp for adaptive scheduling must
70+
not itself fetch, cancel the periodic timer, or count a menu-originated refresh as an adaptive timer tick.
71+
- Never make an automatic provider, account, workspace, or credential-source selection.
72+
- Never bypass provider-specific auth, prompt, coalescing, or failure gates.
73+
74+
## Deterministic policy
75+
76+
Use a pure `AdaptiveRefreshPolicy` that returns the delay before the next ordinary provider-batch refresh.
77+
78+
```swift
79+
struct AdaptiveRefreshPolicy: Sendable {
80+
struct Input: Sendable, Equatable {
81+
let now: Date
82+
let lastMenuOpenAt: Date?
83+
let lowPowerModeEnabled: Bool
84+
let thermalState: ProcessInfo.ThermalState
85+
}
86+
87+
enum Reason: String, Sendable {
88+
case recentInteraction
89+
case warm
90+
case idle
91+
case longIdle
92+
case constrained
93+
}
94+
95+
struct Decision: Sendable, Equatable {
96+
let delay: Duration
97+
let reason: Reason
98+
}
99+
100+
func nextDelay(for input: Input) -> Decision
101+
}
102+
```
103+
104+
Policy table, evaluated after startup and after every completed or skipped timer tick:
105+
106+
| Condition, first match wins | Next delay | Reason |
107+
|---|---:|---|
108+
| Low Power Mode or serious/critical thermal state | 30 minutes | `constrained` |
109+
| Menu opened at or after 5 minutes ago, including a future clock-adjusted timestamp | 2 minutes | `recentInteraction` |
110+
| Menu opened more than 5 minutes and at most 1 hour ago | 5 minutes | `warm` |
111+
| Menu opened more than 1 hour and less than 4 hours ago | 15 minutes | `idle` |
112+
| No menu open recorded, or last open at least 4 hours ago | 30 minutes | `longIdle` |
113+
114+
Bounds are part of the contract:
115+
116+
- minimum automatic interval: 2 minutes;
117+
- maximum automatic interval: 30 minutes;
118+
- one timer task at a time;
119+
- one global provider-batch refresh at a time;
120+
- canceled timers do not launch work;
121+
- settings changes cancel and replace the pending timer.
122+
123+
The policy deliberately excludes quota level, provider latency, error count, account choice, time-of-day, and content
124+
change rate. Those signals require new durable state or provider-specific semantics and do not belong in the first
125+
experiment.
126+
127+
## Scheduler integration
128+
129+
Keep scheduling inside `UsageStore`; do not add a second scheduler abstraction.
130+
131+
1. Extend `RefreshFrequency` with `adaptive`; its fixed `seconds` value remains `nil`.
132+
2. Replace the fixed `startTimer()` loop with a loop that asks a small helper for the next delay:
133+
- fixed mode returns the selected fixed delay;
134+
- manual mode returns no delay and ends the task;
135+
- adaptive mode calls `AdaptiveRefreshPolicy`.
136+
3. After sleeping, check cancellation and call the existing `refresh()` batch path.
137+
4. Recompute the next delay after the batch completes.
138+
5. Record menu-open time through a minimal callback owned by `UsageStore` or a dedicated in-memory signal object. Do
139+
not couple the policy to `NSMenu`, menu descriptors, account switchers, or rendering state.
140+
141+
`NSBackgroundActivityScheduler` is out of scope. Current refresh choices include intervals below the range where that
142+
API is intended to help, and using two scheduling mechanisms would make cancellation and exact timing harder to audit.
143+
Revisit it only with separate energy measurements and a design for launch/relaunch behavior.
144+
145+
## State, privacy, and observability
146+
147+
The first experiment stores no interaction history.
148+
149+
- Keep `lastMenuOpenAt` in memory; reset it on launch.
150+
- Read Low Power Mode and thermal state at decision time.
151+
- Log only the selected delay and stable `Reason` code through the existing local logger.
152+
- Do not log or store provider identity, account identity, email, workspace, path, credentials, response data, or menu
153+
content for scheduling.
154+
- Do not add analytics or send refresh-policy data off device.
155+
156+
This avoids a new retention policy, migration, deletion UI, and behavioral profile. Persistent history requires a new
157+
privacy and storage decision; it is not an incremental extension of this proposal.
158+
159+
## Failure and auth behavior
160+
161+
Adaptive scheduling controls only when the existing batch is requested. All provider behavior remains downstream:
162+
163+
- background interaction context remains the default for automatic work;
164+
- provider refresh coalescing remains authoritative;
165+
- background work must not request interactive Keychain or browser authentication;
166+
- a failed batch does not trigger an immediate policy retry;
167+
- missing/error menu rows continue to use the existing delayed, rendered-provider-only retry path;
168+
- manual refresh remains the only path allowed to opt into user-initiated behavior.
169+
170+
Do not add a scheduler-level failure backoff in the first experiment. Providers have different failure semantics, and
171+
the current failure gates primarily control error publication rather than retry eligibility. A shared backoff would
172+
need a separate contract for partial batch success.
173+
174+
## Implementation sequence
175+
176+
Each step should be independently reviewable:
177+
178+
1. Add the pure policy and table-driven tests; no settings or runtime changes.
179+
2. Add the `Adaptive` setting and localization, default unchanged.
180+
3. Teach the existing timer to request fixed/manual/adaptive delays.
181+
4. Wire the in-memory menu-open signal without changing `scheduleOpenMenuRefresh(for:)`.
182+
5. Add local reason-code logging and documentation.
183+
6. Package and validate the opt-in mode before considering a default change.
184+
185+
Do not add target adapters, outcome databases, account/workspace prediction, learned models, visible ordering changes,
186+
or menu prewarming as part of these steps.
187+
188+
## Required tests
189+
190+
### Pure policy
191+
192+
- every table boundary, including exactly 5 minutes, 1 hour, and 4 hours;
193+
- Low Power Mode wins over recent interaction;
194+
- serious and critical thermal states select 30 minutes;
195+
- nominal/fair thermal states do not force the constrained branch;
196+
- future or clock-adjusted menu timestamps are treated as recent and never produce a negative delay;
197+
- every decision stays within the 2-to-30-minute bounds.
198+
199+
### Timer integration
200+
201+
- fixed and manual modes retain current behavior;
202+
- adaptive mode sleeps for the policy result and recomputes after refresh;
203+
- changing frequency cancels the old timer without one extra refresh;
204+
- overlapping timer ticks do not overlap `UsageStore.refresh()`;
205+
- launch with no menu history begins at 30 minutes;
206+
- menu-open signal changes the next decision but does not itself start a batch.
207+
208+
### Menu regression
209+
210+
- opening a fresh menu still does not schedule a menu-originated refresh;
211+
- missing/error rows still refresh only rendered providers;
212+
- enabling refresh-all-on-open still refreshes every enabled provider without resetting the periodic timer;
213+
- adaptive and menu-open work arriving together still coalesce per provider instead of duplicating requests;
214+
- closing a menu still controls deferred prompt-capable dashboard work;
215+
- in-flight provider work still coalesces;
216+
- manual refresh remains user-initiated.
217+
218+
Use stubs and test stores. Do not run live providers, browser-cookie imports, or Keychain reads for policy validation.
219+
220+
## Acceptance and rollback
221+
222+
Before changing the default, a separate proposal must provide measured evidence. Minimum evidence:
223+
224+
- deterministic replay tests show fewer scheduled batches than the 5-minute baseline during idle traces;
225+
- active traces never schedule slower than the existing 5-minute default;
226+
- no regression in menu-open responsiveness or prompt safety;
227+
- packaged opt-in use shows understandable reason logs and no timer overlap.
228+
229+
Rollback is deleting the `Adaptive` option and policy helper. Fixed/manual scheduling and stored fixed selections remain
230+
valid because the experiment does not migrate them or change their raw values.
231+
232+
## Explicit non-decisions
233+
234+
Approval of this document does not approve:
235+
236+
- making adaptive refresh the default;
237+
- changing the refresh-all-on-open default or its existing provider/auth behavior;
238+
- per-provider, per-account, per-workspace, or per-source scheduling;
239+
- persistent interaction or outcome history;
240+
- `NSBackgroundActivityScheduler` adoption;
241+
- account/menu ordering changes;
242+
- EWMA, Bayesian, bandit, ranker, or language-model decisions;
243+
- new telemetry collection or external analytics.
244+
245+
Any of those requires its own evidence and product/privacy review.

0 commit comments

Comments
 (0)