Skip to content

feat: add adaptive refresh frequency option#1861

Open
hhh2210 wants to merge 3 commits into
steipete:mainfrom
hhh2210:codex/adaptive-refresh-policy
Open

feat: add adaptive refresh frequency option#1861
hhh2210 wants to merge 3 commits into
steipete:mainfrom
hhh2210:codex/adaptive-refresh-policy

Conversation

@hhh2210

@hhh2210 hhh2210 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Implements the adaptive refresh decision record from #1739, as rewritten there. Posting the implementation so it can be judged against the record's acceptance criteria; I understand the merge gate is still your product sign-off on the record itself, so treat this as ready-when-you-are.

What it does

A new "Adaptive" choice in the Refresh cadence picker, listed after the fixed intervals. Defaults are untouched and fixed/manual behave exactly as before.

The decision lives in AdaptiveRefreshPolicy (new file, 70 lines), a pure function from input to decision with the first-match-wins table from the record: Low Power Mode or thermal serious/critical gives 30 min; a menu opened within the last 5 minutes gives 2 min; within the hour, 5 min; under 4 hours, 15 min; otherwise (or with no menu open recorded) 30 min. Every input lands inside the 2 to 30 minute bounds, including negative/future ages from clock adjustments.

The only signal is one in-memory lastMenuOpenAt timestamp set in menuWillOpen. It is never persisted, resets on launch, and never triggers a refresh by itself. Each timer tick logs reason=<code> delay=<n>s under a new adaptive-refresh category and nothing else. The record's non-decisions are respected: no history, no per-account state, no prewarming, no scheduler changes, no ML, not the default.

One line outside the strictly additive scope: the fixed-interval timer branch was missing the post-sleep Task.isCancelled check, so changing frequency mid-sleep fired one spurious refresh (the cancel wakes the sleep early and try? swallows it). Reproduced in a test, fixed with the same guard the adaptive branch uses. Happy to split this into its own PR if you prefer.

Update (9e497b5): interval-derived heuristics stay active in adaptive mode

Clawsweeper's review caught a real gap here, and Copilot flagged a log nit; both are fixed.

RefreshFrequency.seconds is nil for adaptive, and four call sites read that nil as "manual, no normal poll". The worst one: the reset-boundary scheduler from #1857 disabled itself entirely in adaptive mode. The others: the OpenAI web staleness interval fell to its 120s floor instead of scaling with the cadence, and the persistent-CLI-session idle windows collapsed to their 180s floor, which would churn CLI sessions between adaptive ticks.

The fix follows the review's recommended option. A new UsageStore.normalRefreshIntervalForHeuristics() resolves manual to nil (unchanged), fixed intervals to their seconds, and adaptive to the delay the policy would pick right now from live signals. Three call sites route through it. The fourth, ProviderRegistry.specs, runs before a UsageStore exists, so adaptive maps there to a named nominal constant (300s, the policy's warm tier) via ProviderRegistry.nominalRefreshInterval(for:).

Regression coverage is the new AdaptiveRefreshHeuristicsTests suite (9 tests, 13 cases). Each of the four call sites has a test that goes red if it is reverted to .seconds; I verified that by applying each revert as a mutation and running the suite (one focused failure per call site, and mutating the helper's adaptive branch back to nil turns four tests red). The reset-boundary case runs through the real refresh() pipeline with a stubbed codex fetch, in both adaptive mode (schedules) and manual mode (still doesn't).

The log line is now exactly reason=<code> delay=<n>s with the category carrying the identification, as promised below (the first push had a redundant "adaptive refresh: " prefix; thanks Copilot).

Runtime proof

Built this branch with Scripts/package_app.sh debug (0.38.1 build 95, bundle com.steipete.codexbar.debug, isolated defaults domain), set the cadence to Adaptive, and ran it:

$ log stream --level debug --style compact --predicate 'subsystem == "com.steipete.codexbar" AND category == "adaptive-refresh"'
Timestamp               Ty Process[PID:TID]
2026-07-04 04:29:01.313 Db CodexBar[33407:a7afc7] [com.steipete.codexbar:adaptive-refresh] reason=longIdle delay=1800s
2026-07-04 04:32:00.919 Db CodexBar[33407:a7afc7] [com.steipete.codexbar:adaptive-refresh] reason=recentInteraction delay=120s
2026-07-04 04:34:20.183 Db CodexBar[33407:a7afc7] [com.steipete.codexbar:adaptive-refresh] reason=recentInteraction delay=120s

Reading the three lines: launch has no menu-open history, so the first decision is longIdle (30 min). At 04:32 I opened the status menu and re-selected Adaptive in Settings; the cadence change restarts the timer, and the fresh decision sees the seconds-old menu open, so recentInteraction (2 min). The 04:34:20 line is the next tick firing on its own after the 120s sleep plus the refresh, recomputing from live signals. Nothing redacted; reason and delay are all these lines ever carry, by design.

The picker in Settings, running this build:

General settings pane with Refresh cadence set to Adaptive

The policy-table transitions themselves (warm/idle/constrained boundaries) are covered exhaustively in AdaptiveRefreshPolicyTests rather than re-proven against the wall clock.

Tests

27 new cases across three files in the original push: the full policy table with boundary ages, priority ordering, and a bounds sweep over every input combination; timer cancellation tested in isolation from the settings-observer refresh path (restart at the same frequency, exact-delta assertions); and an integration test that drives a real menuWillOpen and checks the signal lands on the store. Mutation-checked: deleting either cancellation guard or the noteMenuOpened() call turns at least one test red. Plus the 13 heuristics cases described above.

Validation

  • make check: 0 violations in 1239 files
  • make test: all 45 shards pass
  • swift build and swift build --build-tests: clean
  • All 21 locale catalogs gained a translated "Adaptive" label; plutil -lint clean, locale coverage suites pass
  • node Scripts/check-documentation-links.mjs: OK, 142 local links

docs/refresh-loop.md gained an "Adaptive mode" section documenting the table, signal ownership, and the heuristics interval contract.

Refs #1739.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KmMSEXFQeQHSBZjUyK1hoU

Implements the adaptive refresh decision record proposed in steipete#1739:
a pure first-match-wins policy table (2-30 min bounds) driven by
menu-open recency, Low Power Mode, and thermal state. Opt-in via a
new Refresh frequency choice; fixed/manual behavior unchanged.

Also adds the missing post-sleep cancellation check to the fixed
interval timer branch, which previously fired one spurious refresh
whenever the frequency changed mid-sleep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmMSEXFQeQHSBZjUyK1hoU
Copilot AI review requested due to automatic review settings July 3, 2026 16:17
@clawsweeper

clawsweeper Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed July 3, 2026, 5:09 PM ET / 21:09 UTC.

Summary
Adds an opt-in Adaptive refresh cadence with policy/timer/menu-open wiring, heuristic integration, localization, docs, tests, and a fixed-timer cancellation guard.

Reproducibility: not applicable. this is a feature PR rather than a current bug report. Verification comes from source/proof review against the accepted design and the supplied packaged-app log and screenshot proof.

Review metrics: 3 noteworthy metrics.

  • Diff Surface: 35 files changed, +910/-9. The PR spans scheduling, settings, localization, docs, and tests, so maintainers should review more than one helper.
  • Localization Surface: 21 catalogs changed. The new user-facing Refresh cadence option is translated across the existing localization set.
  • Test Surface: 3 test files added, 1 changed. Coverage targets the policy table, timer behavior, heuristic consumers, and menu-open signal wiring.

Root-cause cluster
Relationship: canonical
Canonical: #1861
Summary: This PR is the active runtime implementation candidate for the merged adaptive-refresh decision record; related refresh work is design or adjacent baseline behavior rather than a replacement implementation.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] Merging ships a new opt-in automatic provider-batch cadence; the default remains unchanged, but users who enable it may see different provider-call workload and energy behavior that unit tests cannot fully settle.
  • [P1] The live PR check rollup still had Linux and macOS jobs pending, so final merge should wait for required checks rather than relying only on PR-body validation.

Maintainer options:

  1. Merge After Runtime Sign-Off (recommended)
    A maintainer can accept the opt-in provider-batch cadence and energy/workload tradeoff once required checks pass.
  2. Ask For Workload Evidence
    If provider-call frequency or battery impact remains uncertain, request additional packaged-app logs or measurements before merging.
  3. Pause Runtime Shipping
    If the runtime experiment is not ready to ship, keep the design record and pause or close this implementation PR.

Next step before merge

  • [P2] The remaining action is maintainer runtime/product sign-off and waiting for pending CI, not a narrow automated code repair.

Security
Cleared: No concrete security or supply-chain issue found; the diff touches app scheduling/settings code, local reason-only logging, localization, docs, and tests without dependency, workflow, secret, or auth-boundary changes.

Review details

Best possible solution:

Merge after maintainer sign-off confirms the opt-in runtime cadence tradeoff and required checks are green; otherwise pause this implementation while keeping the merged design record.

Do we have a high-confidence way to reproduce the issue?

Not applicable: this is a feature PR rather than a current bug report. Verification comes from source/proof review against the accepted design and the supplied packaged-app log and screenshot proof.

Is this the best way to solve the issue?

Yes for the code shape: the implementation is narrow, default-off, and follows the accepted bounded design. The remaining question is whether maintainers want to ship this runtime experiment now.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 386de1eaddd9.

Label changes

Label justifications:

  • P2: This is a normal-priority user-facing feature with limited blast radius because it is opt-in and leaves the default cadence unchanged.
  • merge-risk: 🚨 other: Merging ships a new opt-in background provider-call cadence whose real-world workload and energy behavior are not fully settled by unit tests.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (logs): The PR body supplies after-fix packaged-app log-stream output showing Adaptive decisions and a Settings screenshot showing Adaptive selected; the screenshot was inspected.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body supplies after-fix packaged-app log-stream output showing Adaptive decisions and a Settings screenshot showing Adaptive selected; the screenshot was inspected.
Evidence reviewed

What I checked:

  • Repository policy read: AGENTS.md was read fully and applied to the review, especially the focused SwiftPM/testing guidance and the instruction to avoid live provider or Keychain-prompting validation unless explicitly requested. (AGENTS.md:1, 386de1eaddd9)
  • VISION sign-off gate: VISION.md classifies new features and meaningful behavior changes as needing sign-off, so this opt-in runtime cadence remains a maintainer decision even though the design record has landed. (VISION.md:13, 386de1eaddd9)
  • Current main lacks runtime Adaptive: Current main's RefreshFrequency contains manual plus fixed 1, 2, 5, 15, and 30 minute modes, with no Adaptive enum case or label branch. (Sources/CodexBar/SettingsStore.swift:6, 386de1eaddd9)
  • Current timer is fixed-interval only: Current main's UsageStore.startTimer cancels existing work, reads refreshFrequency.seconds once, sleeps the fixed interval, and refreshes in a loop. (Sources/CodexBar/UsageStore.swift:713, 386de1eaddd9)
  • Accepted design is separate from runtime implementation: The merged decision record allows an opt-in 2-to-30 minute Adaptive cadence and says runtime implementation, tests, localization, and packaged proof remain a separate change. (docs/predictive-refresh-policy.md:17, 386de1eaddd9)
  • PR head adds the Adaptive setting surface: The PR head adds .adaptive to RefreshFrequency, keeps seconds nil for adaptive, leaves missing defaults at five minutes, and adds a localized label branch. (Sources/CodexBar/SettingsStore.swift:6, fb4d30e0f78d)

Likely related people:

  • steipete: Recent current-main history includes the reset-boundary refresh merge, refresh-on-open merge, and repository-owner sign-off context for the adaptive design and runtime decision. (role: recent area contributor and product sign-off owner; confidence: high; commits: 5701ac6036d0, c3463404eff8, 913b1a6412b5; files: Sources/CodexBar/UsageStore.swift, Sources/CodexBar/UsageStore+ResetBoundaryRefresh.swift, Sources/CodexBar/StatusItemController+Menu.swift)
  • hhh2210: Authored the merged adaptive-refresh decision record on current main and the implementation branch under review, so they have direct context for the accepted policy and code shape. (role: accepted design author and implementation contributor; confidence: high; commits: 692da32ecbbb, 551a684960e5, 9e497b568542; files: docs/predictive-refresh-policy.md, Sources/CodexBar/AdaptiveRefreshPolicy.swift, Sources/CodexBar/UsageStore.swift)
  • dstier-git: The merged refresh-on-open setting touched the same settings and menu-open behavior that this adaptive cadence must preserve. (role: adjacent refresh settings contributor; confidence: medium; commits: 79c24a4ca7bf, 1f3bb5804812, c3463404eff8; files: Sources/CodexBar/SettingsStore.swift, Sources/CodexBar/StatusItemController+Menu.swift, Sources/CodexBar/MenuOpenRefreshPlan.swift)
  • pavbar: The reset-boundary refresh behavior that this PR preserves in Adaptive mode was credited as pavbar's implementation in the merged maintainer PR. (role: adjacent reset-boundary contributor; confidence: medium; commits: 5701ac6036d0; files: Sources/CodexBar/UsageStore+ResetBoundaryRefresh.swift, Tests/CodexBarTests/UsageStoreResetBoundaryRefreshTests.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in Adaptive refresh frequency to CodexBar’s refresh loop, implementing a deterministic policy (2–30 min) driven by recent menu interaction plus power/thermal constraints, with logging, documentation, localization, and extensive test coverage.

Changes:

  • Introduces AdaptiveRefreshPolicy (pure input→decision) and wires it into UsageStore.startTimer() for per-tick adaptive delay computation.
  • Records an in-memory lastMenuOpenAt signal from StatusItemController.menuWillOpen(_:) and logs each adaptive decision under a new log category.
  • Adds targeted unit/integration tests, updates refresh-loop documentation, and localizes the new “Adaptive” label across catalogs.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift Adds integration test asserting menuWillOpen records the menu-open signal on the store.
Tests/CodexBarTests/AdaptiveRefreshTimerTests.swift New tests covering timer behavior across manual/fixed/adaptive modes and cancellation correctness.
Tests/CodexBarTests/AdaptiveRefreshPolicyTests.swift New tests covering policy table boundaries, priority ordering, and bounds guarantees.
Sources/CodexBarCore/Logging/LogCategories.swift Adds adaptive-refresh logging category constant.
Sources/CodexBar/UsageStore+AdaptiveRefresh.swift New wiring helpers: compute/log adaptive decision and apply DEBUG sleep override.
Sources/CodexBar/UsageStore.swift Adds adaptive logger, menu-open timestamp signal, DEBUG counters/overrides, and adaptive timer branch; fixes fixed-timer cancellation guard.
Sources/CodexBar/StatusItemController+Menu.swift Records menu-open signal (store.noteMenuOpened()) in menuWillOpen.
Sources/CodexBar/SettingsStore.swift Adds .adaptive refresh frequency option and localized label mapping.
Sources/CodexBar/Resources/ar.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/ca.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/de.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/en.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/es.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/fa.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/fr.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/id.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/it.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/ja.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/ko.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/nl.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/pl.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/sv.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/th.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/tr.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/uk.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/vi.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings Adds refresh_adaptive translation.
Sources/CodexBar/AdaptiveRefreshPolicy.swift New pure policy implementing the acceptance-criteria decision table.
docs/refresh-loop.md Documents Adaptive mode behavior, policy table, signal ownership, and logging constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 21 to 32
@@ -23,6 +28,7 @@ enum RefreshFrequency: String, CaseIterable, Identifiable {
case .fiveMinutes: 300
case .fifteenMinutes: 900
case .thirtyMinutes: 1800
case .adaptive: nil
}
Comment on lines +33 to +35
// Reason and delay only; never provider/account/email/path/credential/response data.
self.adaptiveRefreshLogger.debug(
"adaptive refresh: reason=\(decision.reason.rawValue) delay=\(decision.delay.components.seconds)s")
RefreshFrequency.seconds is nil for adaptive, and four call sites read
that nil as "manual": the reset-boundary scheduler disabled itself, the
OpenAI web staleness interval fell to its 120s floor, and both
persistent-CLI-session idle windows collapsed to 180s. Add
UsageStore.normalRefreshIntervalForHeuristics() (manual nil, fixed
seconds, adaptive = live policy delay) and route those call sites
through it; ProviderRegistry specs are built before a UsageStore
exists, so adaptive maps to the policy's nominal 300s there. Drop the
"adaptive refresh: " log prefix so the line is exactly
"reason=<code> delay=<n>s".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmMSEXFQeQHSBZjUyK1hoU
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. labels Jul 3, 2026
@hhh2210

hhh2210 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 3, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 3, 2026
The stubbed-codex fixture left the live-system codex account unpinned,
so the account-scoped apply guard read the real ~/.codex of whatever
machine ran the tests: on a developer machine with a codex login the
canned snapshot applied, on CI runners (no ~/.codex) both guard
identities stayed unresolved and the liveSystem branch dropped the
snapshot, leaving no reset-boundary candidate. Pin
_test_liveSystemCodexAccount to a fixture account and give the canned
snapshot the same email, matching the pattern the existing
account-scoped refresh tests use.

Verified locally with HOME pointed at an empty directory (simulating
the CI runner) and re-verified the call-site mutation still turns the
adaptive boundary test red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmMSEXFQeQHSBZjUyK1hoU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants