Open agent change during trial

Task 8938 · Done · Coder · created · updated

## Original requirement (verbatim, epic 8925) > When a new customer is on trial, they signed up for some agents, but they are not paying for them for 14 days. Let's give them unlimited agents and keep a tally of what was the max until now compared to what they signed up for. Something like 10/3. > > This needs to be web researched and analyzed and graphically represented in the sidebar with an unobtrusive Upgrade button that allows to increase the number of agents workin. Settled inputs: the BA PRD (Task 8923, decisions D1–D7, assumptions AS1–AS6, criteria 1–10) and the Architecture (Task 8924, §3 components, §4 decomposition). This child is one row of that §4 table. The contracts quoted below are pinned; do not re-design them. ## Settled decisions this child relies on (verbatim from the PRD) - **D1. The "max until now" number** is peak **demand** since the trial started: the maximum over time of **agents running + ready tasks the dispatcher held back only because of the org's agent cap** (the trial ceiling). Tasks waiting only for node or worktree capacity are not counted. The operator asked for "the maximum agents that were willing to run, but were limited by plan". - **D2. "Unlimited" during the trial** means the runner-slot cap is lifted to the plan's **Max agents**. When the plan has no Max agents, a new operator-set per-plan **trial agent ceiling** applies. A blank ceiling falls back to a fixed default of 10. Node and worktree capacity still bound the count. - **D3. Upgrade during a trial** raises the agent count without charging now. The new count is billed when the trial converts. The count field is prefilled with the peak. This lifts the `IN_TRIAL` refusal for the agent count only. Plan, interval and hosting changes stay refused during a trial. (Architecture amendment Q1: the trial agent change moves the count **both ways**, within the plan's min/max.) - **D4. The meter shows only while the subscription is `trialing`.** It is hidden after conversion, cancellation or expiry. - **D5. Audience.** Every member of the org sees the meter. Only Owners and Admins (`isOrgAdminSeat`) see the Upgrade button. - **D6. Trial end.** At conversion the cap returns to the paid agent count. Running work is never killed; only new dispatch is limited. In the last 3 days of the trial, if peak is greater than signed-up, the meter turns warning-coloured and states the gap, e.g. "Trial ends in 2d — used 10, paying for 3". - **D7. Scope** covers **per-agent trial plans only**. Flat plans have no customer-chosen "signed up for" number and are unchanged. - **AS1.** An org override (`orgs.limit_runner_slots`) still wins over the trial ceiling. **AS2.** The `active_worktrees` cap must not hold trial dispatch below the trial ceiling. **AS3.** A staging org shares its parent's ledger; its demand counts toward the parent's meter and the meter shows on both. **AS5.** Peak is a high-water mark persisted per org and per trial, reset when a new trial starts. ## This child: C3 — Agent count change during the trial Approach (Architecture §3.6, verbatim): * `_changeable(..., allow_trial: bool = False)`: skips `refuse_in_trial` only when asked. `preview_agents` passes `True`; `change_agents` reaches it through `_spend_preview`, which gains `allow_trial` and is called as `_spend_preview(..., kind, allow_trial=(kind == "agents"))` — so `change_interval` (the other `_spend_preview` caller), plan and hosting changes keep refusing. * `preview_agents` when `live.sub.status == "trialing"`: no Stripe preview call; store the preview with `due = 0` and return `Preview(amount_due=0, next=unit_amount × agents, at=trial_end)` so the dialog can say "Nothing now · $X from {trial_end}". Both directions allowed (Q1); range still `org_plans.agent_refusal`. * `change_agents` when trialing: `outbound.subscription_agents(..., trialing=True)` → `{"items":[{id, quantity}], "proration_behavior": "none"}`, **no** `payment_behavior`. Same audit row (`billing.agents`), same `_write` → `on_entitlement` updates `agents_bought`. Never touches `trial_end`. * `IN_TRIAL` docstring updated: agents now open during a trial, the reason (bill at conversion, D3). * Web: `apps/web/src/components/billing/ChangeAgentsDialog.tsx` takes an optional `initialCount` (stepper start; default `agents.count`) and, from the preview's `amount_due == 0` + trial date, renders the trial line. `SubscriptionCard` shows "Change agents" when state is `active` **or `trialing`**. Material risk (Architecture §5.1): Stripe's $0 trial-update behaviour is implied, not stated. The test-clock oracle (criterion 7) is this child's gate: if Stripe produces a non-zero invoice or ends the trial, do NOT merge — record the observed Stripe behaviour on this task and fail it; no fallback is assumed. Dev fixtures: this child runs alongside C1 and needs no C1 code for its oracles. Reuse C1's fixtures if they already exist; otherwise create the same ones yourself through the console (Products → a per-agent plan with trial days; Customers → a test tenant org with a trialing subscription on N=3 on dev's Stripe test account) and name them in the PR body so C1/C4 reuse them. Tests: `POST /api/org/billing/agents/preview` + `/agents` while trialing → 2xx, quantity moves both ways, gateway called with `proration_behavior: none` and no `payment_behavior`; plan/interval/hosting still 409 `IN_TRIAL`; Vitest for `initialCount` and the trial line. Credentials & reach: (verbatim rows from the Architecture §7; the dev console host is `app.dev.jaah.ai` — the PRD's "dev.jaah.ai" means this host; operate dev only through the console UI) | Oracle | Credential / identity / environment | Status | |---|---|---| | Dev dispatch, sidebar, Upgrade, plan editor, trial-end move (criteria 1–6, 8–10) | `jaah/debug/main/operator-login` (operator; switches into tenants; Owner role in a test tenant; plain member created by that Owner in the console), dev CP `https://app.dev.jaah.ai` | verified — `credential_get` ok; `GET /` 200, `GET /api/auth/me` 401 (reached, unauthenticated) | | Stripe test-clock first-invoice check (criterion 7) | `jaah/debug/stripe-dev` (test mode secret key), `api.stripe.com` | verified — `credential_get` ok; host reached (HTTP 404 on bare `/v1/`) | | Unit/API/Vitest/Playwright tests | worktree `./dc` stack only | verified — nothing beyond the worktree | | Dev fixtures for criteria 1–3, 6, 8 (a per-agent plan with trial days, a trialing test-tenant subscription on dev's Stripe test account, a node with ≥ 8 free slots for that tenant) | same operator login, console only | deferred — fixture state, not access: the C1 builder creates it through the console (Products → plan; Customers → org + `set_trial_end`) before its dev check; scaled down if fewer slots exist (e.g. ceiling 3 vs N=1) | Out of scope: - Do NOT change trial length logic (`billing_plans.trial_days`, the `trial_days()` eligibility rules) or trial eligibility and reuse rules (`self_serve_rules.trial_used`). - Do NOT lift `IN_TRIAL` for plan, interval or hosting changes. Only the agent-count change is opened. - Do NOT auto-bill the peak, and do NOT change the agent count without an admin's explicit confirmation. - Do NOT stop, pause or evict running tasks or sessions at trial end or on a cap change. - Do NOT change caps or behaviour for flat (non-per-agent) plans, non-trialing subscriptions, or the other caps except as AS2 requires. - Do NOT add a post-trial usage meter or a usage analytics page. - Do NOT restructure the sidebar footer line (badge, name, version, Help). The meter is an addition above it. - Do NOT add nag modals, toasts or emails for upgrade prompting. - Deferred, do NOT build: post-trial used-vs-bought meter; operator Customers view of trial peak; `customer.subscription.trial_will_end` notices. - Do NOT build the sidebar meter or its Upgrade entry point (C4); this child only makes the dialog and the Subscription page work during a trial. - Do NOT touch caps, the placer or the peak (C1). Done means: - **6 (API + Subscription page half). Agent change during the trial.** Confirming the Change agents flow during the trial succeeds without a charge. The Subscription page then shows the new agent count, with billing at trial end. The trial does not end. → API test: `POST /api/org/billing/agents` during trialing returns 2xx and updates the quantity. Plan, interval and hosting changes still return 409 `IN_TRIAL`. → Verify on dev: as Owner of a trial org with N=3, open Subscription → Change agents, set 8, confirm. Observe no invoice or charge, the trial-ends date unchanged, and the Subscription page shows 8 agents. The dialog shows "Nothing now" and the amount from the trial-end date (not a $0 total with no explanation). - **7. Billing at conversion uses the upgraded count.** → API/Stripe test-clock test (`docs/integrations/stripe.md`): a trial upgraded 3→8, advanced past the trial end, produces a first invoice for 8 agents. Also assert: no non-zero invoice before the trial end, and the subscription is still `trialing` right after the update. - **10. On by default, real deployment.** No feature flag gates criteria 6–7. → Verify on `app.dev.jaah.ai` with no manual toggle.

Outcome

Trial agent-count change (both ways, unprorated, $0; trial end bills new count) + dialog "Nothing now" line. Quick gate, 1275 billing tests, Vitest green; Stripe test clock: 3→8 billed $160 at conversion. Merged #6611 (2ad8286); its deploy superseded. Dev Owner check deferred (needs deploy + trial fixture). Visual check done locally: readable on desktop/phone. Memory: proposed 1 gotcha.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)