-
Notifications
You must be signed in to change notification settings - Fork 421
fix(warmup): warm paid-to-free transitions #1825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Soju06
merged 2 commits into
Soju06:main
from
HulianBuligon:codex/warm-free-plan-transition
Aug 20, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| ## Context | ||
|
|
||
| See `proposal.md` for motivation. The usage updater mutates and synchronizes the | ||
| selected account only after its existing paid-to-Free confirmation policy is | ||
| satisfied. The warm-up service currently sees only the post-refresh account and | ||
| requires matching canonical before/after windows, so it cannot distinguish a | ||
| confirmed plan transition from an account that was already Free. | ||
|
|
||
| The existing `usage_reset_confirmed` guard protects ordinary reset detection | ||
| from cross-window comparisons and timestamp drift. The transition path must not | ||
| weaken that guard. | ||
|
|
||
| ## Goals / Non-Goals | ||
|
|
||
| **Goals:** | ||
|
|
||
| - Carry enough refresh-scoped evidence to identify a confirmed paid-to-Free | ||
| transition without introducing new persistent state. | ||
| - Require the monthly candidate to have been written by the same refresh and | ||
| to pass the existing availability, account, and global opt-in gates. | ||
| - Reuse the existing monthly warm-up identity and atomic claim. | ||
|
|
||
| **Non-Goals:** | ||
|
|
||
| - Changing paid-to-Free confirmation or ordinary same-window reset detection. | ||
| - Adding settings, schema, migrations, retry queues, or periodic backfill. | ||
| - Sending warm-up traffic to inactive or non-opted-in accounts. | ||
|
|
||
| ## Decisions | ||
|
|
||
| ### Snapshot the selected account plan before refresh | ||
|
|
||
| The scheduler will preserve the selected account's normalized pre-refresh plan | ||
| and pass it to warm-up evaluation after reloading the account. A transition is | ||
| eligible only when the snapshot is a recognized paid plan and the persisted | ||
| post-refresh plan is `free`. | ||
|
|
||
| Alternative considered: infer a transition from `secondary` to `monthly` usage | ||
| rows. That would incorrectly classify already-Free accounts whose first monthly | ||
| sample arrives after stale secondary history. | ||
|
|
||
| ### Require a monthly sample written during the same refresh | ||
|
|
||
| The fallback candidate will accept only the selected long-window row when its | ||
| canonical window is `monthly`, it has a reset deadline, and its `recorded_at` is | ||
| at or after the refresh start. It will apply the existing minimum-availability | ||
| gate before returning a candidate. | ||
|
|
||
| Alternative considered: use the latest persisted monthly row regardless of | ||
| age. That could warm stale quota after an unrelated plan metadata update. | ||
|
|
||
| ### Keep the transition as a fallback to normal reset detection | ||
|
|
||
| The service will first evaluate the existing same-window reset candidate. Only | ||
| when that returns no candidate for the configured long window will it evaluate | ||
| the paid-to-Free transition. The resulting candidate uses `window="monthly"` | ||
| and the monthly `reset_at`, so the existing atomic attempt claim provides | ||
| deduplication. | ||
|
|
||
| Alternative considered: alter `usage_reset_confirmed` to allow cross-window | ||
| transitions. That would weaken a safety guard used by status recovery and | ||
| ordinary warm-up paths. | ||
|
|
||
| ## Risks / Trade-offs | ||
|
|
||
| - [A process exits after persisting plan and usage but before warm-up] → The | ||
| transition can be missed, matching the current event-triggered reset path; | ||
| avoid new persistence until stronger delivery semantics are required. | ||
| - [A future updater mutates plan before confirmation] → Keep regression coverage | ||
| at scheduler/service boundaries and rely on the updater's existing durable | ||
| two-observation confirmation contract. | ||
|
|
||
| ## Migration Plan | ||
|
|
||
| No data migration is required. Deploy the code normally; rollback restores the | ||
| previous behavior without changing stored warm-up attempts or usage history. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| ## Why | ||
|
|
||
| A confirmed paid-to-Free plan change can replace the account's prior paid quota | ||
| window with a newly available monthly window. The existing same-window safety | ||
| guard correctly rejects arbitrary cross-window comparisons, but it also skips | ||
| the opted-in warm-up for this confirmed plan transition. | ||
|
|
||
| ## What Changes | ||
|
|
||
| - Preserve the selected account's plan type across one background refresh. | ||
| - Treat a confirmed paid-to-Free transition that writes a fresh available | ||
| monthly sample as a long-window warm-up candidate. | ||
| - Keep ordinary reset detection restricted to matching canonical windows and | ||
| keep single, unconfirmed Free observations ineligible. | ||
| - Add regressions for the consumer-visible warm-up attempt and the safety | ||
| boundaries around unchanged plans and stale monthly history. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### New Capabilities | ||
|
|
||
| None. | ||
|
|
||
| ### Modified Capabilities | ||
|
|
||
| - `usage-refresh-policy`: Allow an opted-in long-window warm-up after a | ||
| confirmed paid-to-Free transition opens a fresh monthly quota window. | ||
|
|
||
| ## Impact | ||
|
|
||
| - Affected code: `app/core/usage/refresh_scheduler.py` and | ||
| `app/modules/limit_warmup/service.py`. | ||
| - Affected tests: focused scheduler and limit warm-up tests. | ||
| - No API, schema, migration, setting, dependency, dashboard, or deployment | ||
| change. |
56 changes: 56 additions & 0 deletions
56
openspec/changes/warm-free-plan-transition/specs/usage-refresh-policy/spec.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: Confirmed paid-to-Free transitions warm the new monthly window | ||
|
|
||
| When background usage refresh confirms that an opted-in active account changed | ||
| from a recognized paid plan to `free`, and that confirming refresh writes a | ||
| fresh monthly usage sample with a reset deadline and enough available quota for | ||
| the configured warm-up threshold, the system SHALL attempt one long-window | ||
| warm-up for that monthly quota window. Eligibility MUST NOT depend on the usage | ||
| percentage reported before the plan change. | ||
|
|
||
| The plan-transition exception SHALL apply only to an actual paid-to-Free change | ||
| confirmed by the refresh that wrote the monthly sample. It MUST NOT apply to a | ||
| single unconfirmed Free observation, an account that was already Free, or a | ||
| monthly sample left over from an earlier refresh. Ordinary same-window reset | ||
| detection MUST remain unchanged. The durable warm-up identity SHALL remain the | ||
| account, canonical `monthly` window, and monthly reset deadline. The confirming | ||
| monthly sample MUST report `used_percent < 100`; the configured minimum- | ||
| available threshold MAY impose a stricter lower usage limit. | ||
|
|
||
| #### Scenario: Confirmed paid-to-Free transition warms fresh monthly quota | ||
|
|
||
| - **GIVEN** an active opted-in account whose stored plan is a recognized paid plan | ||
| - **WHEN** background usage refresh confirms its transition to `free` | ||
| - **AND** that confirming refresh writes a monthly sample with a reset deadline and enough available quota | ||
| - **THEN** the system attempts one warm-up identified by the account, `monthly` window, and monthly reset deadline | ||
|
|
||
| #### Scenario: Previous usage percentage does not gate plan-transition warm-up | ||
|
|
||
| - **GIVEN** an active opted-in paid account whose previous selected quota sample was not exhausted | ||
| - **WHEN** background usage refresh confirms its transition to `free` and writes an eligible fresh monthly sample | ||
| - **THEN** the system attempts the monthly warm-up regardless of the previous usage percentage | ||
|
|
||
| #### Scenario: One unconfirmed Free observation does not warm | ||
|
|
||
| - **GIVEN** an active opted-in account whose stored plan is a recognized paid plan | ||
| - **WHEN** one background usage refresh reports `free` without satisfying downgrade confirmation | ||
| - **THEN** no plan-transition warm-up is attempted | ||
|
|
||
| #### Scenario: Already-Free account does not use the plan-transition exception | ||
|
|
||
| - **GIVEN** an active opted-in account whose stored plan was already `free` | ||
| - **WHEN** background usage refresh writes its first monthly sample without confirming a plan change | ||
| - **THEN** no plan-transition warm-up is attempted | ||
|
|
||
| #### Scenario: Stale monthly history does not warm after a plan change | ||
|
|
||
| - **GIVEN** an active opted-in account whose transition from a paid plan to `free` is confirmed | ||
| - **WHEN** the latest monthly sample predates the confirming refresh | ||
| - **THEN** no plan-transition warm-up is attempted | ||
|
|
||
| #### Scenario: Existing durable identity deduplicates the transition warm-up | ||
|
|
||
| - **GIVEN** a warm-up attempt already exists for an account, `monthly` window, and monthly reset deadline | ||
| - **WHEN** the same confirmed paid-to-Free transition is evaluated again | ||
| - **THEN** no second warm-up request is sent for that durable identity |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| ## 1. Refresh-scoped transition evidence | ||
|
|
||
| - [x] 1.1 Snapshot the selected account's plan before background usage refresh. | ||
| - [x] 1.2 Pass the pre-refresh plan map and refresh timestamp into long-window warm-up evaluation. | ||
|
|
||
| ## 2. Monthly transition candidate | ||
|
|
||
| - [x] 2.1 Add a paid-to-Free fallback candidate that requires a fresh available monthly sample. | ||
| - [x] 2.2 Preserve ordinary same-window reset detection and the existing durable monthly claim. | ||
|
|
||
| ## 3. Regression coverage | ||
|
|
||
| - [x] 3.1 Prove a confirmed paid-to-Free scheduler refresh sends one monthly warm-up regardless of prior usage. | ||
| - [x] 3.2 Cover unconfirmed or unchanged Free plans, stale monthly history, availability gating, and deduplication. | ||
|
|
||
| ## 4. Validation | ||
|
|
||
| - [x] 4.1 Run focused scheduler and limit warm-up tests. | ||
| - [x] 4.2 Run Ruff format/check, Ty, strict OpenSpec validation, and diff hygiene checks. |
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.