| index 72cbeb8b3..e52a648b5 100644 |
| --- a/docs/superpowers/specs/2026-10-20-plans-caps-packs-design.md |
| +++ b/docs/superpowers/specs/2026-10-20-plans-caps-packs-design.md |
| @@ -135,7 +135,8 @@ them as configuration, not constants. |
| | Idle window before sleep | 90 s (1 to 2 min **(Paul)**) | 15 min **(Paul)** | none | 15 min | |
| | Memory / CPU per app | 256 MB / 0.5 CPU | 512 MB / 1 CPU (today's default) | none | the existing 1 GB / 2 CPU children tier | |
| | Awake hours per month (account) | 100 | 1,500 | none in v1 | n/a | |
| -| Database + files on box (per app) | 1 GB | 10 GB | +10 GB (account pool, see 5.4) | 2x cap, and box safety | |
| +| Database + files on box (per running thing, 5.4) | 1 GB | 10 GB | +10 GB to every running thing | 2x cap, and box safety | |
| +| Sidecar workers per app | 1 | 5 | none in v1 | 5 | |
| | Object storage (per account) | operator-approved only, 1 GB if approved | 25 GB self-serve | +50 GB | 2x cap | |
| | Email per day (account) | 50 | 1,000 | +1,000 | 10,000 without an operator | |
| | Bandwidth per month (account) | 10 GB | 200 GB | +200 GB | 2x cap, then throttled | |
| @@ -156,9 +157,15 @@ cap, what is counted, where it is enforced, and what "refused" means. |
| |
| **Counted:** apps, temporary apps, and customer instances (including |
| seeded and imported). Each counts as one. Sidecar workers do not count |
| -against the cap, but a free account may run at most one sidecar per app, |
| -and sidecars obey the plan's idle window (5.2). Fleets are customer-owned |
| -boxes, not apps; free accounts cannot create fleets. |
| +against the running-things cap; they have their own per-app cap (table |
| +4.2: 1 on free, 5 on Personal), enforced at `handleAPISidecarCreate` |
| +(`apisidecars.go:216`), and they obey the plan's idle window (5.2). Fleets |
| +are customer-owned boxes, not apps; free accounts cannot create fleets. |
| + |
| +**Grace and creation (codex 1.2).** Refusing to create something new is |
| +the cap itself, not a cut-off, so it has no grace: the warning at 80% |
| +is the notice. Grace protects what already runs. It applies whenever a |
| +lowered limit would take something away from something running. |
| |
| **Enforced at:** |
| - `claimAppWith` (`internal/console/server.go:4052`), the single claim |
| @@ -167,9 +174,14 @@ boxes, not apps; free accounts cannot create fleets. |
| `lockAccount`, refused with 409 and `"code":"cap"`. |
| - `restoreApp` (`restore.go:55`) and the claims-mode restore |
| (`claim_restore.go:120`), which do not pass through the claim. |
| -- `provision.CreateRecord` (`internal/provision/provision.go:882`) for |
| - instances, reached from `apiinstances.go:1664`, `apiinstanceseed.go:341`, |
| - `apiinstanceimport.go:172` and `domainattach.go:237`. |
| +- Instance creation. `provision.CreateRecord` |
| + (`internal/provision/provision.go:882`) is **not** a common choke point |
| + (codex 1.10): claims-mode creation goes through `finishClaimCreate` |
| + instead (`apiinstances.go:1658`). The check belongs in the console |
| + handlers before either write: `handleAPIInstanceCreate` |
| + (`apiinstances.go:987`, which the seeded and imported variants pass |
| + through) and the first-route attach (`domainattach.go:237`), under the |
| + same account lock as the app claim so the two counts cannot race. |
| - Direct mode skips the claim (`server.go:~4030`). The builder must either |
| count those apps when their rows are filled from the bucket and apply |
| the over-cap stages, or refuse direct-mode creation for plan-governed |
| @@ -215,10 +227,17 @@ runs. Paid keeps one minute. |
| pingers, open WebSockets and mining. The source is the per-account |
| `instance_hours` figure the hourly economics pass already writes to |
| `usage/accounts/<acct>/<month>.json` (`economicspass.go:378-397,835`). It |
| -is approximate and that is acceptable: it only gates. After grace, the |
| -account's apps get the shortest idle window the platform allows (and |
| -WebSocket upgrades are refused) until the month rolls or the account |
| -upgrades. They still wake and serve. |
| +is approximate and that is acceptable: it only gates. |
| + |
| +A shorter idle window cannot stop an app that is never idle: a steady |
| +pinger, a long stream or an open WebSocket keeps `inflight` above zero |
| +(`activator.go:4650`) (codex 1.1). So after grace the account is |
| +**throttled** through the same per-account throttle state as bandwidth |
| +(5.7), its open WebSockets are closed and new upgrades refused, and each |
| +app gets a continuous-awake ceiling (the activator's existing run ceiling, |
| +`MaxRunSeconds`, applied to instances): after 15 minutes awake the app is |
| +put to sleep and wakes on its next request. Apps still wake and serve, |
| +slowly, until the month rolls or the account upgrades. |
| |
| ### 5.3 Memory and CPU |
| |
| @@ -229,8 +248,12 @@ The tier lists that hard-refuse unknown tiers need the new entry: |
| `strict_linux.go:216`, `strict_gen_linux.go:162`, `gen.go:65`, and |
| unit-backed apps' numbers in `caps/render.go`. |
| |
| -There is no grace stage: a ceiling is a ceiling. What is new is the |
| -**explanation**: when an app is OOM-killed under the free tier, the |
| +A new app gets its plan's tier at once. An app that is already running |
| +when its account's limit drops (lapse, downgrade) keeps its old tier |
| +through the 7-day grace and moves to the lower tier at its next start |
| +after grace (codex 1.2). Lowering memory under a running process would |
| +kill it, which is the cut-off without notice the grace rule forbids. What |
| +is new is the **explanation**: when an app is OOM-killed under the free tier, the |
| console's app page and `carlos` CLI status say so in plain words and |
| point at the upgrade. A free app that cannot run in 256 MB should fail |
| loudly and once, not restart-loop silently. |
| @@ -247,16 +270,27 @@ the kernel, so writes fail at the hard limit with no polling and no race. |
| Filesystem quotas fit the policy exactly: a soft limit with a grace timer |
| and a hard limit. The builder chooses the mechanism (ext4 project quotas, |
| an XFS data volume with project quotas, or another), and the choice may |
| -mean new boxes get a data volume built for it. Existing boxes must keep |
| -working without a quota until they are replaced; a box without quota |
| -support falls back to a periodic size check that refuses wakes after grace. |
| - |
| -**Counting for warnings:** a periodic size check per app (the quota's own |
| -usage report where available) feeds the 80% warning and the grace clock. |
| - |
| -Per-app cap or account pool: the cap is per app, and a storage pack raises |
| -the cap of every app on the account. (Proposal; the simpler rule to |
| -enforce in the kernel.) |
| +mean new boxes get a data volume built for it. |
| + |
| +**The unit is the running thing (codex 1.5).** Each app or instance has |
| +its own data directory on exactly one box (`provision.go:2960`), so the |
| +quota is per data directory, and a storage pack raises the cap of every |
| +running thing on the account. This sells more total disk than an account |
| +pool would, but every running thing is already counted and paid for, and |
| +it needs no allocation across boxes. |
| + |
| +**Boxes without quota support (codex 1.6).** Existing boxes keep working |
| +without a quota until they are replaced. On those boxes the disk cap is |
| +**warn-only**: warnings and reminders, no after-grace stage, and never a |
| +stop or a refused wake for disk. The box-level disk alert and placement's |
| +10% free-disk floor (`boxchoice/policy.go:22`) remain the protection |
| +there. The disk cap's after-grace stage switches on per box, only where |
| +the quota exists. Replacing flagship boxes with quota-capable ones is a |
| +rollout prerequisite for that stage only, not for launch. |
| + |
| +**Counting for warnings:** a periodic size check per data directory (the |
| +quota's own usage report where available) feeds the 80% warning and the |
| +grace clock. |
| |
| **After grace:** writes fail (SQLite sees a full disk). Reads keep |
| working. The app page says why. |
| @@ -282,11 +316,21 @@ v1 (8.6). |
| |
| ### 5.6 Email |
| |
| -The mechanism exists: daily caps by tier (`internal/console/mailcounts.go:67`), |
| -counted from SES events, enforced by pausing the sender's key |
| -(`mail/walk.go:1120`) until the UTC day rolls. What is missing is the plan: |
| -`mailBillingTier` always answers `"free"` (`apimail.go:245`, #226). It |
| -reads the account's effective email limit instead. |
| +Half the mechanism exists: daily caps by tier |
| +(`internal/console/mailcounts.go:67`), counted from SES events, enforced by |
| +pausing a sender's key (`mail/walk.go:1120`) until the UTC day rolls. But |
| +it is **per sender, not per account** (codex 1.11): `enforceMailLimits` |
| +reads one sender's counts against that sender's persisted grant |
| +(`mailcounts.go:253`), and `mailBillingTier` (`apimail.go:245`, always |
| +`"free"`, #226) is only consulted when a grant is created |
| +(`apimail.go:1297`). v1 needs: |
| + |
| +- an account-wide daily count, summed over the account's senders, checked |
| + against the account's effective email limit; at the limit every sender |
| + of the account is paused; |
| +- the per-sender cap kept as it is (an operator's `DailyCap` still works); |
| +- existing grants re-derived when the account's limit changes (plan, |
| + pack, lapse), not only at grant creation. |
| |
| Email's cap resets daily, so a week-long grace does not fit. Its grace is |
| **within the day**: warn at 80%, allow up to 2x the cap, pause at 2x. The |
| @@ -329,15 +373,23 @@ for bandwidth and awake hours. Every grace has a hard ceiling behind it |
| (table 4.2): grace never means unlimited growth. |
| |
| State lives in the console (one row per account per cap: first over, |
| -grace ends, last notified), so a reminder is sent once per period and a |
| -console restart does not reset a clock. |
| +grace ends, last notified, notice received), so a reminder is sent once |
| +per period and a console restart does not reset a clock. |
| + |
| +**What counts as notice (codex 1.4).** Notice is received when any of |
| +these is recorded for an owner of the account, after the cap went over: |
| +the warning email was accepted for delivery; the owner loaded a console |
| +page that showed the banner; or the owner ran a CLI command that printed |
| +the warning. **An after-grace stage starts only when grace has ended and |
| +notice was received at least 3 days earlier.** An unattended account |
| +whose owners were never reached stays in grace. Two things do not wait |
| +for notice: hard ceilings (they protect the box and every other tenant, |
| +not our economics) and refusing to create something new. |
| |
| **Dependency:** the console cannot email account owners today; giving it |
| an SES sending grant is an open security ruling (the same one holding |
| console invite delivery, see memory `console-invite-delivery`). Until it |
| -is settled, warnings go to the console and CLI only, and v1 must not |
| -start an after-grace stage for a cap whose owners could only have been |
| -told by email. |
| +is settled, notice is only the console banner and CLI warning. |
| |
| ## 7. How limits reach the boxes |
| |
| @@ -372,6 +424,10 @@ take customer apps down on a bucket hiccup. |
| accounts are created as paid (checkout first) or by an operator. |
| `carlos accounts create --owner` (operator, bucket-direct) is exempt. |
| Address farming is expected; detecting farms is later, not v1. |
| + The rule applies at creation only (codex 1.8). A person who owns |
| + several paid accounts that later lapse keeps them all, each on free |
| + limits: each was paid for, and the alternative means deleting or |
| + charging. Ownership transfers are not checked against it either. |
| 2. **Never-sleep paths refused** (5.2). |
| 3. **Everything that runs counts** (5.1). |
| 4. **Phishing and malware.** v1 keeps one apps domain for free and paid |
| @@ -394,9 +450,20 @@ take customer apps down on a bucket hiccup. |
| reaped, amadan #122). Reported per account on the operator economics |
| view. No caps in v1. |
| 7. **Sponsored accounts have limits too.** "Cover current usage" means a |
| - sponsored account's effective limits are the larger of Personal's caps |
| - and its usage on the day plans switch on, recorded once. Increases are |
| - reviewed separately (settled policy). |
| + sponsored account's effective limits are, per limit, the larger of |
| + Personal's cap and its measured usage, recorded once when plans switch |
| + on (codex 1.9): |
| + - counts and sizes (running things, disk, storage): as measured on |
| + switch-on day; |
| + - recurring figures (email, bandwidth, awake hours): the highest full |
| + month in the three months before switch-on, from the existing |
| + monthly usage statements (`usage/accounts/<acct>/<month>.json`); |
| + email from the per-day counts, highest day. |
| + A recorded sponsored limit may exceed a table 4.2 hard ceiling; it is |
| + then an explicit operator-approved figure on that account, listed for |
| + Paul before switch-on. Increases after that are reviewed separately |
| + (settled policy). Nothing is invented: an account with no statement |
| + history gets Personal's caps. |
| |
| ## 9. Checkout and packs |
| |
| @@ -420,6 +487,11 @@ account keeps its paid limits. When the subscription ends, the account |
| drops to free limits and section 6 applies to every cap it is now over, |
| with its normal grace. Nothing is deleted. |
| |
| +Paid-only features already in use when an account drops to free |
| +(attached custom domains, fleets, schedules under 15 minutes, a second |
| +free-plan-excess sidecar) (codex 1.7): **open, for Paul** (see the end of |
| +this document). |
| + |
| ### 9.3 Bands |
| |
| The original model scaled price by an affordability band (four bands per |
| @@ -449,8 +521,16 @@ Not a merge. Copy onto current main, adapt, and review as new code. |
| `resourcebudget`, `awsbilling`, `authority`, invoices, calculation, tax |
| split, statements, overage preferences, `month.go`, `lines.go`), bands |
| and countries, the legacy-EUR path, sponsored cohort enrollment and |
| - observation (the house flag covers sponsorship), and |
| - `billingretirement` unless the builder shows v1 needs it. |
| + observation, and `billingretirement` unless the builder shows v1 needs |
| + it. |
| +- **Sponsorship needs a new path (codex 1.12).** The harvested house flag |
| + does not cover it on its own: at `165472b8` `queueEntitlementTx` |
| + (`internal/console/entitlementstore.go:219`) publishes nothing for an |
| + account with no subscription, and sponsored accounts never check out. |
| + v1 needs a subscription-independent plan assignment (an operator sets |
| + an account's plan, with its recorded sponsored limits from 8.7) that |
| + writes the same limits record. Self-hosted deployments without a billing |
| + provider use the same path (section 10). |
| - **Main rework:** the old code assumes one subscription item everywhere |
| (`billingservice.go:939`, `billingquantity.go:1015`). Packs mean several |
| items. Reconciliation, intent matching and price recognition must handle |
| @@ -530,3 +610,31 @@ All live steps need Paul. Dev before flagship (roll order rule). |
| - Delivery slicing, beyond the order: limits and records, then caps, then |
| notices and grace, then checkout, then the page. |
| - Copy for every refusal and warning (through copy review). |
| + |
| +## 14. Review record |
| + |
| +Round 1, Astra builder check at `5ff0e014`, findings committed unedited as |
| +`CODEX-FINDINGS-PLANS-CAPS-1.md`. Numbering here: questions 1 to 9 are |
| +codex 1.1 to 1.9; the three citation errors, in order, are codex 1.10 to |
| +1.12. All twelve are accepted: |
| + |
| +| Finding | Where answered | |
| +| --- | --- | |
| +| 1.1 awake-hours cap cannot stop a never-idle app | 5.2: throttle, close WebSockets, continuous-awake ceiling | |
| +| 1.2 grace vs creation refusal and memory reductions | 5.1, 5.3 | |
| +| 1.3 sidecars unbounded on paid | table 4.2, 5.1 | |
| +| 1.4 what counts as notice | section 6 | |
| +| 1.5 storage unit | 5.4: per running thing | |
| +| 1.6 boxes without quotas | 5.4: warn-only there | |
| +| 1.7 paid-only features after downgrade | open, for Paul | |
| +| 1.8 one free account per person after lapse | 8.1: creation only | |
| +| 1.9 sponsored "current usage" measurement | 8.7 | |
| +| 1.10 `CreateRecord` is not the instance choke point | 5.1 | |
| +| 1.11 email caps are per sender | 5.6 | |
| +| 1.12 house flag publishes no limits without a subscription | 9.4 | |
| + |
| +## Open, for Paul |
| + |
| +1. **Paid-only features after a downgrade** (codex 1.7). When a paid |
| + account lapses to free, what happens to custom domains, fleets and |
| + short schedules it already uses? |