feat(billing): add usage plans, metering, and enforcement - #721
Conversation
The AGPL text was the stock FSF copy with the author placeholder never filled in, so the MIT notice names ThinkEx Inc. to match the entity the footer and the privacy, cookie, and terms pages already use. Relicensing needs agreement from the outside contributors who hold copyright in the existing code; this commit only changes the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.116.0 added bundle-size and startup CPU reporting to `wrangler check startup`; the worker is at 6.72 MB gzip against the 10 MB limit, so the size readout is worth having before deploys start failing on it. Ignore *.cpuprofile: the command writes one into the repo root, and Wrangler also emits one whenever a deploy fails on startup time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Patch and minor bumps across tiptap 3.29.2, ai 7.0.48, drizzle, posthog, shiki, knip, and the vite-plus toolchain. Three packages are held back because patches/ carries hand-written diffs against their dist output: @cloudflare/think, agents, and @embedpdf/plugin-zoom. Bumping them needs the patches re-authored first. Pin @modelcontextprotocol/sdk to 1.29.0 rather than tracking 1.30.0: agents 0.19.0 depends on 1.29.0, and two copies in the tree make the two McpServer types nominally distinct, which breaks createMcpHandler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move the DOMPurify pin to 3.4.12, continuing 99e2e7f: 3.4.11 was the patched release then and is the vulnerable one now. Route @esbuild-kit/core-utils to esbuild 0.25.12. drizzle-kit reaches esbuild only through the deprecated @esbuild-kit packages, which pin 0.18.20, inside the advisory range. Scoped rather than global so nothing else moves; `drizzle-kit check` still reads drizzle.config.ts. The other open alerts cleared on their own in the version sweep: brace-expansion, fast-uri, postcss, and shell-quote all resolve to patched releases now, and fast-xml-parser was already at 5.10.1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The default config block shared the production worker name while binding the staging R2 bucket and Vectorize index alongside the production D1. A `wrangler deploy` without `--env` therefore targeted the live worker and swapped its storage out from under it: users on the production database, reading an empty staging bucket. Rename the default worker so an env-less deploy lands somewhere harmless. Production and staging blocks are untouched, and the build scripts already pass CLOUDFLARE_ENV. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps in the extraction and intake events. LlamaParse credits were parsed out of the job response and persisted into the projection metadata, but never reached PostHog, so the cost of the pipeline could only be estimated. provider_mode records the tier we asked for, which the cost optimizer is free to disagree with per page; credits_used is what was actually billed. Both events also fell back to a synthetic distinct id when no actor was present — the workflow instance for extraction, the request id for intake — registering a brand new person per run. One month read as 247 uploaders when 25 people had uploaded anything. Add processPerson to the shared capture helper and set it from whether a real user is attached, so the event is still recorded but no person is created. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 1-4 cost levels claimed to come from each model's output-token price, but Haiku and Flash were both marked 1 alongside Luna despite costing 4.2x and 2.5x more on output. Output price alone also understates the models we send the most tokens to, since it ignores input and cached input entirely. Price one typical chat step (~15k input at ~65% cache hit, ~500 output) against the live catalog at ai-gateway.vercel.sh/v1/models and store the real multiplier rather than a bucket: Luna 1, Flash 3, Haiku 5, Sonnet 5 / Terra / Gemini Pro 10. These double as credit weights if usage is ever metered. Internal only — a raw multiplier means nothing to a user who isn't paying per message, so the picker shows billingTier instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The picker rendered cost as a fourth segment bar identical to Intelligence and Speed, which flipped polarity silently — more filled meant better for two of them and worse for the third — and flattened a 10x spread into "4 of 4". Show the billing tier instead. A raw multiplier is meaningless to someone not paying per message, whereas Premium maps directly to the allowance a plan grants. Both tiers render so the row keeps one height while hovering between models, and the tier sits on its own row rather than beside the name, which otherwise forced long names to truncate. The premium badge variant is a neutral fill rather than a hue: every accent in the palette is either already claimed (amber is warning, blue is info) or too light for small text, and brand colors aren't defined yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the catalog as code (autumn.config.ts, pushed with the atmn CLI) so plans live in the repo and the same file targets sandbox and production, rather than being copied between environments in a dashboard. Separates the environments. Production and local dev shared one Autumn key, so dev sessions landed in the same customer list as real users. AUTUMN_SECRET_KEY is now sandbox-only and AUTUMN_PROD_SECRET_KEY production-only, resolved by one shared helper so the better-auth plugin and the usage tracker cannot disagree about which environment they are in. Deliberately not switched on import.meta.env.PROD: Vite sets that for every build, so staging would report into production. A deployment missing its key skips tracking, which is recoverable, instead of writing to the wrong environment, which is not. Meters file uploads, which were granted but never consumed. Counted when extraction is requested rather than when bytes land, because extraction is what costs money and every extraction routes through one caller. Per upload rather than per page: nobody knows a PDF's page count before uploading, and median upload is 5 pages against a p90 of 31. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Settings was a route, so opening it from a workspace navigated the user out of whatever they were doing. It is now a dialog driven by a validated `settings` search param: a search param rather than a hash because a hash never reaches the server, so a cold load would render the page and only pop the dialog in after hydration. /settings stays as a redirect for existing links. Adds a plan and usage panel reading live balances from Autumn. Usage bars fill as you spend rather than draining toward zero, and the number counts the same direction — a gauge emptying toward a limit makes people ration an allowance they will almost never reach, which suppresses conversion more than it saves. Rebuilds the account panel on Item rows. The previous read-only text inputs looked editable and reserved space for fields a guest account does not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The landing page and autumn.config.ts both state prices, so they can drift. Note where the real numbers live so Pro doesn't ship quoting a price nobody is charged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate existed as a stub returning allowed for everyone, so plans were granted, tracked, and displayed while nothing applied them. It returns the model the turn should run on rather than a yes/no, so callers use what they're handed instead of branching on whether a downgrade happened. An empty tier falls through to the other one: premium spent lands on `auto`, and `auto` reaches for premium when standard is spent. That second direction matters more than it looks — `auto` is the model nobody changes, so if it blocked the moment standard ran out, the default experience would be the first thing to break while a premium balance sat unused. Fails open when Autumn is unreachable. Not being able to read a balance must never stop someone using the product. The decision is a pure function so the whole matrix is testable without a network or a customer; the async wrapper only fetches balances. It costs one check in the common case and a second only once a tier is empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without this the fallback is silent: someone picks Sonnet, sends, and gets an Auto answer with no idea why. The server gate is authoritative but only runs after they've committed to a message. `check()` reads the cached customer with no network call, which is what makes it cheap enough to sit above the composer. Advisory only — the client's copy can be stale and is trivially bypassed. Two states, and both describe the next message rather than a balance: the tier is spent so another model will answer, or nothing is left to send with. There is deliberately no "running low" warning. A count ticking down in the most looked-at part of the screen is what makes people ration an allowance they will never reach, and premium running out is not even a wall. Scoped to the selected model. Telling someone on Auto that their premium balance is empty is noise about a bucket they aren't spending from. The picker carries availability in the detail panel rather than on every premium row: it's one tier-level fact, and the list would state it three times. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uploads were metered but never checked, so the cap was decorative and the allowance in settings could only ever count up. Checked before the presigned URL is issued rather than at completion, so someone over their cap never transfers bytes we then reject. No fallback here, unlike chat. The only cheaper path is keeping the free local parse and skipping the paid one, which returns worse text with nothing marking it as degraded — and a user cannot tell degraded extraction from a bad product. Blocking is the honest option. Fails open when Autumn is unreachable, matching the message gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pro read "Coming soon" while being purchasable in-app, and both plans described their limits as "limited" and "more" — which tells a prospect nothing and can't be compared. Uses the numbers autumn.config.ts actually grants, in the same order on both cards so the two are readable side by side. The `ponytail:` note is gone since the launch it deferred has happened, replaced by one that says where the numbers have to stay in sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews. |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
|
Warning Review limit reached
Next review available in: 2 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughThe pull request adds Autumn billing plans, environment-specific secret handling, AI and file-upload metering, account billing settings, usage-limit messaging, and related telemetry updates. ChangesAutumn billing integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant AiChatPromptInput
participant workspaceAiUsage
participant Autumn
participant ai-thread
User->>AiChatPromptInput: select model and submit message
AiChatPromptInput->>workspaceAiUsage: check tier allowance
workspaceAiUsage->>Autumn: check entitlement
Autumn-->>workspaceAiUsage: allowance and reset data
workspaceAiUsage-->>AiChatPromptInput: allow, fallback, or block
ai-thread->>workspaceAiUsage: record resolved model usage
workspaceAiUsage->>Autumn: track usage unit
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb60e901ef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // May differ from what was selected: an empty tier falls through to the | ||
| // other one rather than failing the turn. | ||
| modelId = access.modelId; |
There was a problem hiding this comment.
Preserve the fallback model across continuations
When the selected tier is exhausted, the initial turn replaces modelId with access.modelId, but a browser/tool continuation re-enters beforeTurn with ctx.continuation === true, initializes modelId from the original request again, and skips this assignment. The resumed request can therefore run a model from the tier that was already denied; reuse the active usage context's resolved model for every continuation of the turn.
Useful? React with 👍 / 👎.
| // Before the presigned URL, so nobody uploads bytes we then reject. | ||
| const access = await checkWorkspaceFileUploadAccess({ env, userId }); |
There was a problem hiding this comment.
Meter document imports before enforcing the upload cap
This check applies to every validated upload, including the document plans used for CSV, TSV, Markdown, code, and text files, but successful completion only reaches the new trackWorkspaceFileUploadUsage call through requestWorkspaceFileExtraction in the separate file branch. Document imports therefore never decrement file_uploads and can be uploaded indefinitely despite the advertised 50/500-upload limits; either meter those successful imports or exclude them consistently from the cap.
Useful? React with 👍 / 👎.
| if (allowance.willFallBack) { | ||
| const fallbackName = getWorkspaceAiChatModelById("auto").name; |
There was a problem hiding this comment.
Name the actual model used for standard-tier fallback
When a standard model's balance is exhausted but premium remains, resolveWorkspaceAiMessageAccess falls back to claude-sonnet, whereas this notice always says Auto will answer. For Auto itself this produces the contradictory message “Auto will answer ... no Auto left” and hides that the turn will consume premium allowance; derive the displayed fallback from the selected tier using the same mapping as the server.
Useful? React with 👍 / 👎.
Greptile SummaryThis change adds Autumn-backed plans, usage enforcement, billing controls, model-tier fallback, and related telemetry. A focused upload completion test verified that successful document imports create workspace documents without recording Confidence Score: 4/5
What T-Rex did
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/components/landing/PricingSection.tsx (1)
9-36: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winMake the pricing limits contract executable.
pricingPlansduplicates the Free and Pro limits fromautumn.config.ts. The comment documents drift, but it does not prevent drift. If the catalog changes, the landing page can promise different limits or units than the server enforces. Add a shared public metadata source or a test/build check, and verify that these values represent message counts rather than usage credits.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/landing/PricingSection.tsx` around lines 9 - 36, Make the Free and Pro limits in pricingPlans derive from or be validated against the canonical autumn.config.ts allowances through a shared public metadata source or test/build check. Ensure the displayed values are message counts, not usage-credit amounts, and preserve the existing plan presentation while preventing future drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/account/components/PlanBillingSection.tsx`:
- Around line 25-26: Update PlanBillingSection to destructure and handle the
useCustomer() error state before deriving or rendering plan details: when error
is present and isLoading is false, render the component’s unavailable/retry
state instead of Free and “Not included.” Guard billing actions, including
attach and openCustomerPortal usage, so they remain disabled or unavailable
until customer exists.
In `@src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx`:
- Around line 36-46: Update the willFallBack notice in AiChatAllowanceNotice to
derive the fallback model using the exported tier-flip logic from
resolveWorkspaceAiMessageAccess in workspace-ai-access.ts, rather than
hardcoding getWorkspaceAiChatModelById("auto"). Ensure the displayed model
matches the backend fallback, including standard-tier exhaustion switching to
claude-sonnet while premium remains.
In `@src/features/workspaces/extraction/workspace-file-extraction-workflow.ts`:
- Around line 151-152: Update the extraction workflow around provider.extract,
writeWorkspacePageProjection, and upsertFileProjection to retain the enhanced
extraction credit metadata before persistence operations begin. In the enclosing
failure path, report the captured credits when extraction completed
successfully, and use creditsUsed: null only when enhanced extraction did not
complete.
In `@src/integrations/autumn/client.ts`:
- Around line 1-10: Rename the Autumn integration module from client.ts to
client.server.ts to enforce the server-only boundary, and update every import of
`#/integrations/autumn/client` to `#/integrations/autumn/client.server`. If client
code requires only the AutumnCustomerFields type, move that type into a
server-independent types-only module and update those type imports accordingly.
In `@src/routes/api/v1/workspaces`.$workspaceId.file-upload.ts:
- Around line 89-102: Make upload admission atomic by replacing the
non-consuming check in the upload route with Autumn’s atomic check-and-consume
call using sendEvent: true, ensuring it runs only once before creating the
direct-upload session. In
src/routes/api/v1/workspaces.$workspaceId.file-upload.ts (89-102), update the
admission flow; in src/integrations/autumn/workspace-file-usage.ts (61-78),
remove or bypass any duplicate upload-allowance consumption; in
src/features/workspaces/extraction/request-workspace-file-extraction.ts (38-46),
retain consumption for extraction completion/failure settlement so queued work
still records its usage.
---
Nitpick comments:
In `@src/components/landing/PricingSection.tsx`:
- Around line 9-36: Make the Free and Pro limits in pricingPlans derive from or
be validated against the canonical autumn.config.ts allowances through a shared
public metadata source or test/build check. Ensure the displayed values are
message counts, not usage-credit amounts, and preserve the existing plan
presentation while preventing future drift.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 635b4415-2a8f-47be-a628-0b0239faf70f
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (41)
.dev.vars.example.gitignoreLICENSEautumn.config.tsdocs/ENVIRONMENT.mdpackage.jsonpnpm-workspace.yamlsrc/components/UserProfileDropdown.tsxsrc/components/landing/PricingSection.tsxsrc/components/landing/visuals/ModelsVisual.tsxsrc/components/ui/badge.tsxsrc/features/account/components/AccountSection.tsxsrc/features/account/components/AccountSettingsDialog.tsxsrc/features/account/components/DeleteAccountSection.tsxsrc/features/account/components/PlanBillingSection.tsxsrc/features/account/components/SettingsPage.tsxsrc/features/workspaces/ai/ai-thread.tssrc/features/workspaces/ai/models.tssrc/features/workspaces/ai/use-workspace-ai-allowance.tssrc/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsxsrc/features/workspaces/components/ai-chat/AiChatModelPicker.tsxsrc/features/workspaces/components/ai-chat/AiChatPromptInput.tsxsrc/features/workspaces/extraction/request-workspace-file-extraction.tssrc/features/workspaces/extraction/workspace-file-extraction-observability.tssrc/features/workspaces/extraction/workspace-file-extraction-workflow.tssrc/features/workspaces/upload/workspace-file-intake-observability.tssrc/integrations/autumn/client.tssrc/integrations/autumn/secret-key.tssrc/integrations/autumn/workspace-ai-access.test.tssrc/integrations/autumn/workspace-ai-access.tssrc/integrations/autumn/workspace-ai-usage.tssrc/integrations/autumn/workspace-file-usage.tssrc/integrations/posthog/events.tssrc/integrations/posthog/server.tssrc/lib/auth.server.tssrc/routes/__root.tsxsrc/routes/_protected.tsxsrc/routes/_protected/settings.tsxsrc/routes/api/v1/workspaces.$workspaceId.file-upload.tsworker-configuration.d.tswrangler.jsonc
💤 Files with no reviewable changes (1)
- src/features/account/components/SettingsPage.tsx
| import { Autumn, ResponseValidationError } from "autumn-js"; | ||
| import { eq } from "drizzle-orm"; | ||
|
|
||
| import { user } from "#/db/schema"; | ||
| import { createDbContext } from "#/db/server"; | ||
| import { resolveAutumnSecretKey } from "#/integrations/autumn/secret-key"; | ||
| import { | ||
| logOperationalEvent, | ||
| recordOperationalFailure, | ||
| } from "#/integrations/observability/operational-events"; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Fix the client/server boundary break: rename this file to a .server.ts module.
CI reports that the staging build fails because client code imports "#/db/schema" through this file. This file imports user from #/db/schema and createDbContext from #/db/server at module scope, then exports functions and types from the same module. Any client-side import of this file — even a type-only import that is not stripped correctly — pulls the DB schema into the client bundle.
src/lib/auth.server.ts already uses the .server.ts suffix for the same reason. Apply the same pattern here: rename client.ts to client.server.ts and update every import path (#/integrations/autumn/client → #/integrations/autumn/client.server). If any client component only needs the AutumnCustomerFields type, move that type into a separate types-only module with no server imports so client code doesn't need to reach into the server module at all.
#!/bin/bash
# Find every importer of this module to confirm none run in client-rendered code
# and to enumerate the rename's blast radius.
rg -n --type=ts -C2 "from ['\"]`#/integrations/autumn/client`['\"]"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/integrations/autumn/client.ts` around lines 1 - 10, Rename the Autumn
integration module from client.ts to client.server.ts to enforce the server-only
boundary, and update every import of `#/integrations/autumn/client` to
`#/integrations/autumn/client.server`. If client code requires only the
AutumnCustomerFields type, move that type into a server-independent types-only
module and update those type imports accordingly.
Source: Pipeline failures
| // Before the presigned URL, so nobody uploads bytes we then reject. | ||
| const access = await checkWorkspaceFileUploadAccess({ env, userId }); | ||
|
|
||
| if (!access.allowed) { | ||
| return apiError( | ||
| requestId, | ||
| 402, | ||
| "upload_limit_reached", | ||
| // Gain-framed, and no raw date: the exact reset lives in settings, and a | ||
| // server-formatted date has no idea what locale is reading it. | ||
| "You've used all your file uploads this month. Pro includes 500 a month.", | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
For autumn-js version 1.2.45, does Autumn provide an atomic check-and-consume, reservation, or idempotent usage operation for feature allowances?
💡 Result:
Yes, for autumn-js, the check operation provides an atomic check-and-consume capability [1][2]. When using the check method, you can pass the parameter sendEvent: true (or send_event: true in the SDK's snake_case convention) [1][3][4]. This combines checking the customer's balance against a required amount with the simultaneous recording (consumption) of that usage in a single atomic operation [2][5]. If the balance is sufficient, the specified amount is deducted immediately as part of that same request [2]. Regarding idempotency, the Autumn API and the track operation support idempotency_key headers and parameters to ensure that events are not double-counted during retries [6][7]. While the check operation's atomic send_event mode is designed for concurrent safety [5], ensure your implementation uses appropriate unique keys if your specific application architecture requires further deduplication guarantees during high-concurrency flows [6][7].
Citations:
- 1: https://docs.useautumn.com/api-reference/core/check
- 2: https://useautumn-autumn.mintlify.app/integration/check
- 3: https://docs.atmn.sh/api-reference/core/check
- 4: https://autumn.mintlify.app/api-reference/core/check
- 5: https://docs-v1.useautumn.com/documentation/getting-started/gating
- 6: https://docs.useautumn.com/documentation/customers/edge-cases
- 7: https://deepwiki.com/useautumn/autumn-js/2-core-sdk
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Tracked files matching target names:\n'
git ls-files | rg 'workspaces\.\$workspaceId\.file-upload\.ts|workspace-file-usage\.ts|request-workspace-file-extraction\.ts|autumn|package\.json|pnpm-lock\.(yaml|lock)|yarn\.lock|package-lock\.json'
printf '\nRelevant source snippets:\n'
for f in \
'src/routes/api/v1/workspaces.$workspaceId.file-upload.ts' \
'src/integrations/autumn/workspace-file-usage.ts' \
'src/features/workspaces/extraction/request-workspace-file-extraction.ts'
do
if [ -f "$f" ]; then
printf '\n--- %s (%s lines) ---\n' "$f" "$(wc -l < "$f")"
sed -n '1,180p' "$f" | nl -ba
else
echo "missing $f"
fi
done
printf '\nSearch for checkWorkspaceFileUploadAccess and usage consumer functions:\n'
rg -n "checkWorkspaceFileUploadAccess|trackWorkspaceFileUploadUsage|WorkspaceFileUploadAccess|checkWorkspaceFileUploadUsage|requestWorkspaceFileExtraction" srcRepository: ThinkEx-OSS/thinkex
Length of output: 852
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Python implementation of rg-like source scan:\n'
python3 - <<'PY'
from pathlib import Path
import re
target_files = [
'src/routes/api/v1/workspaces.$workspaceId.file-upload.ts',
'src/integrations/autumn/workspace-file-usage.ts',
'src/features/workspaces/extraction/request-workspace-file-extraction.ts',
]
for f in target_files:
p = Path(f)
print(f'--- {f} exists={p.exists()} ---')
if not p.exists():
continue
text = p.read_text(encoding='utf-8')
lines = text.splitlines()
for i, line in enumerate(lines, 1):
if 'checkWorkspaceFileUploadAccess' in line or 'trackWorkspaceFileUploadUsage' in line or 'const result = await autumn.check' in line or 'sendEvent' in line or 'send_event' in line or 'reservation' in line:
start=max(1,i-12); end=min(len(lines), i+25)
print(f'@@ {f}:{start}-{end} @@')
for j in range(start,end+1):
print(f'{j:5d}: {lines[j-1]}')
print()
print('Autumn check calls with surrounding context:')
for path in Path('src').rglob('*.ts'):
text = path.read_text(encoding='utf-8')
if 'autumn.check' in text:
lines = text.splitlines()
for i,l in enumerate(lines,1):
if 'autumn.check' in l:
for j in range(max(1,i-8), min(len(lines), i+10)+1):
print(f'{path}:{j}: {lines[j-1]}')
print()
print('Search package versions for autumn sdk:')
for p in ['package.json','pnpm-lock.yaml','package-lock.json','yarn.lock']:
pp=Path(p)
if not pp.exists(): continue
txt=pp.read_text(errors='ignore')
if 'autumn' in txt.lower() or '`@autmn`' in txt:
print(f'--- {p} ---')
for line in txt.splitlines():
if 'autumn' in line.lower() or '`@autmn`' in line:
print(line)
PYRepository: ThinkEx-OSS/thinkex
Length of output: 16169
Make upload allowance admission atomic.
Each checkWorkspaceFileUploadAccess call runs before createWorkspaceDirectUploadSession, and check checks the balance without consuming usage. Under concurrency, multiple users can get a presigned upload while one allowance remains, then the separate trackWorkspaceFileUploadUsage call after enqueueing only consumes once. Use Autumn’s atomic check-with-consume path here, such as autumn.check({ ..., sendEvent: true }), and call it only once during upload admission. Keep consumption for extraction usage tied to direct-upload completion/failure handling so queued work still settles when possible.
📍 Affects 3 files
src/routes/api/v1/workspaces.$workspaceId.file-upload.ts#L89-L102(this comment)src/integrations/autumn/workspace-file-usage.ts#L61-L78src/features/workspaces/extraction/request-workspace-file-extraction.ts#L38-L46
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/routes/api/v1/workspaces`.$workspaceId.file-upload.ts around lines 89 -
102, Make upload admission atomic by replacing the non-consuming check in the
upload route with Autumn’s atomic check-and-consume call using sendEvent: true,
ensuring it runs only once before creating the direct-upload session. In
src/routes/api/v1/workspaces.$workspaceId.file-upload.ts (89-102), update the
admission flow; in src/integrations/autumn/workspace-file-usage.ts (61-78),
remove or bypass any duplicate upload-allowance consumption; in
src/features/workspaces/extraction/request-workspace-file-extraction.ts (38-46),
retain consumption for extraction completion/failure settlement so queued work
still records its usage.
There was a problem hiding this comment.
14 issues found across 42 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/features/workspaces/upload/workspace-file-intake-observability.ts">
<violation number="1" location="src/features/workspaces/upload/workspace-file-intake-observability.ts:74">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
The telemetry bug-fix behavior introduced on this line—gating `processPerson` based on `observation.userId` to prevent phantom PostHog persons—is not exercised by any test. Since this is a behavior-change/bug-fix PR and a practical regression assertion exists (mock `capturePostHogServerEvent` and verify `processPerson` is `true` when `userId` is present and `false` when only `requestId` is provided), the rule requires test coverage for the changed behavior.</violation>
</file>
<file name="src/features/workspaces/extraction/workspace-file-extraction-workflow.ts">
<violation number="1" location="src/features/workspaces/extraction/workspace-file-extraction-workflow.ts:275">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
The new `creditsUsed` telemetry reporting is not covered by tests. `getExtractionCreditsUsed` is a pure helper that could regress silently if refactored (e.g., someone changes the `typeof` check), yet no regression-style assertions exercise the three new `creditsUsed` call sites in `recordWorkspaceFileExtractionOutcome`. Since the extraction directory already contains tests for related logic, consider adding a small unit test for the helper and/or verifying the telemetry payload includes the expected `creditsUsed` value after a full extraction run.</violation>
</file>
<file name="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts">
<violation number="1" location="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts:90">
P1: A user at the limit can initiate many uploads concurrently: each request passes this non-reserving check before any completion records usage. Reserve/consume quota atomically when issuing a session (or use a check-and-consume operation), with compensation for expired/failed sessions, so the cap cannot be bypassed by parallel requests.</violation>
<violation number="2" location="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts:92">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
The new upload-quota enforcement branch that returns `402 upload_limit_reached` is not covered by tests. Since this is a behavior-change PR that activates billing on deploy, the untested guard path creates regression risk. A practical regression test would mock `checkWorkspaceFileUploadAccess` to return `{ allowed: false }` and assert the `402` response with code `upload_limit_reached`, and another to return `{ allowed: true }` and assert the presigned-URL flow continues.</violation>
</file>
<file name="src/integrations/autumn/client.ts">
<violation number="1" location="src/integrations/autumn/client.ts:110">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**
The new `trackAutumnUsage` billing integration in `client.ts` is not covered by any test. This is a behavior-change PR adding metering logic, and the rule specifically flags untested changed behavior when regression-style assertions are practical. Consider adding tests that mock the Autumn SDK and assert `getOrCreate`/`track` are called with the correct payloads, cover the `ResponseValidationError` partial-response path, and verify the fail-open operational-logging fallback.</violation>
<violation number="2" location="src/integrations/autumn/client.ts:130">
P2: Concurrent requests near a quota can all pass `check` before background usage events decrement the balance, allowing messages/uploads beyond the allowance. Reserve usage atomically during the access check, then finalize or release that reservation after the model/upload outcome.</violation>
</file>
<file name="src/features/account/components/AccountSettingsDialog.tsx">
<violation number="1" location="src/features/account/components/AccountSettingsDialog.tsx:69">
P3: On phone-sized viewports the settings tabs render as a vertical stack, not the horizontal strip described here, because `orientation` remains `vertical` at every breakpoint. Use responsive orientation/state or override the vertical TabsList/Trigger rules for the mobile layout so the visual layout and keyboard orientation match.</violation>
</file>
<file name="src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx">
<violation number="1" location="src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx:73">
P3: Reset-date formatting now has two identical implementations, so a formatting or timezone correction can diverge between the composer and plan settings. Share one date-formatting utility.</violation>
</file>
<file name="src/routes/__root.tsx">
<violation number="1" location="src/routes/__root.tsx:101">
P2: The client `<AutumnProvider useBetterAuth>` is rendered unconditionally in the root document, but the server-side `autumn()` better-auth plugin it talks to is only mounted when an Autumn secret key resolves (see the conditional spread in createAuth). As a result, in any deployment without a configured Autumn key, the provider will request `/api/auth/autumn`, which doesn't exist in that deployment and returns 404, and the chat-allowance hooks will run against no customer data. Consider gating the provider on the same key-resolution condition (or otherwise detecting that Autumn isn't configured) so keyless environments don't make a guaranteed-failing billing request on every render.</violation>
</file>
<file name="src/features/workspaces/ai/ai-thread.ts">
<violation number="1" location="src/features/workspaces/ai/ai-thread.ts:254">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
The enforcement wiring changes in `beforeTurn`—throwing a reset-date-aware error when blocked and silently falling back to `access.modelId` when a tier is exhausted—are behavior changes that are not exercised by any test in the changed file or elsewhere in the repository. While the underlying `resolveWorkspaceAiMessageAccess` logic has unit tests, the integration of that logic into the turn lifecycle (`modelId` reassignment and error formatting) lacks coverage, and the PR itself notes the wiring "has not been exercised against a genuinely exhausted account." Consider adding a regression-style test that mocks `checkWorkspaceAiMessageAccess` and asserts the resulting `TurnConfig` model or the thrown error message.</violation>
</file>
<file name="src/features/account/components/SettingsPage.tsx">
<violation number="1" location="src/features/account/components/SettingsPage.tsx:16">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
The settings page migration from a full page to a dialog-driven UI is a meaningful behavior change, yet no test in the PR exercises the claimed redirect from the old route. Under the PR's stated behavior ("old route redirects"), a regression-style test asserting that the former /settings route redirects or opens the appropriate dialog would be practical. Consider adding a routing or redirect test for this migration to prevent silent regressions.</violation>
</file>
<file name="src/features/account/components/AccountSection.tsx">
<violation number="1" location="src/features/account/components/AccountSection.tsx:41">
P3: Accounts without a display name show their email twice: once as the title fallback and again as the description. Render the description only when it differs from the selected display name.</violation>
</file>
<file name="LICENSE">
<violation number="1" location="LICENSE:1">
P2: This commit replaces the project's GNU AGPL v3 (copyleft) with an MIT License as a silent byproduct of a billing/pricing feature PR. Switching license type is a material legal change — it removes the AGPL's network-source-disclosure obligations and relicense of existing AGPL-covered code requires consent from every contributor who authored it — and it has nothing to do with the billing feature set. The PR description itself notes the last four commits are unrelated housekeeping, and this belongs in that category. Consider splitting the license change into its own deliberate commit/review (or dropping it from this PR) so it isn't merged implicitly with feature work.</violation>
</file>
<file name="src/integrations/autumn/workspace-ai-usage.ts">
<violation number="1" location="src/integrations/autumn/workspace-ai-usage.ts:46">
P1: Concurrent turns can exceed a depleted allowance because this check does not reserve a unit and usage is recorded only after each response completes. Use an atomic reservation/consumption step before invoking the model (or serialize allowance consumption per customer) so only one final unit can be admitted.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| * the getOrCreate / track / partial-response / failure handling is identical for | ||
| * every metered feature — only the feature id and properties differ. | ||
| */ | ||
| export async function trackAutumnUsage(input: TrackAutumnUsageInput) { |
There was a problem hiding this comment.
P1: Custom agent: Flag AI Slop and Fabricated Changes
The new trackAutumnUsage billing integration in client.ts is not covered by any test. This is a behavior-change PR adding metering logic, and the rule specifically flags untested changed behavior when regression-style assertions are practical. Consider adding tests that mock the Autumn SDK and assert getOrCreate/track are called with the correct payloads, cover the ResponseValidationError partial-response path, and verify the fail-open operational-logging fallback.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/integrations/autumn/client.ts, line 110:
<comment>The new `trackAutumnUsage` billing integration in `client.ts` is not covered by any test. This is a behavior-change PR adding metering logic, and the rule specifically flags untested changed behavior when regression-style assertions are practical. Consider adding tests that mock the Autumn SDK and assert `getOrCreate`/`track` are called with the correct payloads, cover the `ResponseValidationError` partial-response path, and verify the fail-open operational-logging fallback.</comment>
<file context>
@@ -0,0 +1,148 @@
+ * the getOrCreate / track / partial-response / failure handling is identical for
+ * every metered feature — only the feature id and properties differ.
+ */
+export async function trackAutumnUsage(input: TrackAutumnUsageInput) {
+ const autumn = getAutumnClient(input.env);
+
</file context>
| } | ||
|
|
||
| // Before the presigned URL, so nobody uploads bytes we then reject. | ||
| const access = await checkWorkspaceFileUploadAccess({ env, userId }); |
There was a problem hiding this comment.
P1: A user at the limit can initiate many uploads concurrently: each request passes this non-reserving check before any completion records usage. Reserve/consume quota atomically when issuing a session (or use a check-and-consume operation), with compensation for expired/failed sessions, so the cap cannot be bypassed by parallel requests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/api/v1/workspaces.$workspaceId.file-upload.ts, line 90:
<comment>A user at the limit can initiate many uploads concurrently: each request passes this non-reserving check before any completion records usage. Reserve/consume quota atomically when issuing a session (or use a check-and-consume operation), with compensation for expired/failed sessions, so the cap cannot be bypassed by parallel requests.</comment>
<file context>
@@ -85,6 +86,20 @@ async function initiateWorkspaceFileUpload(request: Request, workspaceId: string
}
+ // Before the presigned URL, so nobody uploads bytes we then reject.
+ const access = await checkWorkspaceFileUploadAccess({ env, userId });
+
+ if (!access.allowed) {
</file context>
| const customerFields = await getAutumnCustomerFields(input.userId); | ||
|
|
||
| await autumn.customers.getOrCreate({ | ||
| const chosen = await autumn.check({ |
There was a problem hiding this comment.
P1: Concurrent turns can exceed a depleted allowance because this check does not reserve a unit and usage is recorded only after each response completes. Use an atomic reservation/consumption step before invoking the model (or serialize allowance consumption per customer) so only one final unit can be admitted.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/integrations/autumn/workspace-ai-usage.ts, line 46:
<comment>Concurrent turns can exceed a depleted allowance because this check does not reserve a unit and usage is recorded only after each response completes. Use an atomic reservation/consumption step before invoking the model (or serialize allowance consumption per customer) so only one final unit can be admitted.</comment>
<file context>
@@ -55,151 +31,72 @@ export interface CheckWorkspaceAiMessageAccessInput {
- const customerFields = await getAutumnCustomerFields(input.userId);
-
- await autumn.customers.getOrCreate({
+ const chosen = await autumn.check({
customerId: input.userId,
- ...customerFields,
</file context>
| <Tabs | ||
| value={tab} | ||
| onValueChange={(value) => onTabChange(value as AccountSettingsTab)} | ||
| orientation="vertical" |
There was a problem hiding this comment.
P3: On phone-sized viewports the settings tabs render as a vertical stack, not the horizontal strip described here, because orientation remains vertical at every breakpoint. Use responsive orientation/state or override the vertical TabsList/Trigger rules for the mobile layout so the visual layout and keyboard orientation match.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/account/components/AccountSettingsDialog.tsx, line 69:
<comment>On phone-sized viewports the settings tabs render as a vertical stack, not the horizontal strip described here, because `orientation` remains `vertical` at every breakpoint. Use responsive orientation/state or override the vertical TabsList/Trigger rules for the mobile layout so the visual layout and keyboard orientation match.</comment>
<file context>
@@ -0,0 +1,117 @@
+ <Tabs
+ value={tab}
+ onValueChange={(value) => onTabChange(value as AccountSettingsTab)}
+ orientation="vertical"
+ className="flex-col gap-0 sm:flex-row"
+ >
</file context>
| ); | ||
| } | ||
|
|
||
| function formatResetDate(resetsAt: number | null) { |
There was a problem hiding this comment.
P3: Reset-date formatting now has two identical implementations, so a formatting or timezone correction can diverge between the composer and plan settings. Share one date-formatting utility.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx, line 73:
<comment>Reset-date formatting now has two identical implementations, so a formatting or timezone correction can diverge between the composer and plan settings. Share one date-formatting utility.</comment>
<file context>
@@ -0,0 +1,83 @@
+ );
+}
+
+function formatResetDate(resetsAt: number | null) {
+ if (!resetsAt) {
+ return null;
</file context>
The allowance hook imported WORKSPACE_AI_MESSAGE_FEATURE_IDS from workspace-ai-usage, which pulls in the Autumn client and through it #/db/schema, so the chat composer dragged the db layer into the browser bundle and tripped import protection. Moved the constant to workspace-ai-access, the half with no server-only imports, which the hook and the usage module both already depend on.
There was a problem hiding this comment.
0 issues found across 3 files (changes from recent commits).
Requires human review: Auto-approval blocked by 22 unresolved issues from previous reviews.
Re-trigger cubic
Five separate reports, one shape: the allowance decision was made in one place and re-derived, differently, somewhere else. - A continuation re-read the model from the request body, so every tool round-trip after the first ran on the tier the gate had just rejected and billed it as the model the gate allowed. Reuses the resolved model. - The composer hardcoded Auto as the fallback, which is right only when the empty tier is premium. Running out of standard moves the turn up to Claude Sonnet, so the notice named the wrong model and hid that premium was about to be spent. Both sides now share one mapping. - The upload cap covered every upload but only extraction decremented it, so document imports were gated and never counted. Extraction is the part that costs money, so the gate narrowed to match rather than charging for local conversions. - A failure after extraction reported no credits even though LlamaParse had already billed, understating spend on the runs worth investigating. - An Autumn outage rendered the settings panel as a confident Free with nothing included, which is the one wrong answer a paying customer must never see. Also renames client.ts to client.server.ts. Importing it from a component is what broke the build, and the suffix is how the rest of the repo marks that boundary.
Markdown, CSV, code, and text imports are converted locally and never counted against the cap, but the block message read as though every upload had stopped. Hitting a wall that looks total is what makes people leave instead of upgrade, and this is the only moment they'd learn otherwise.
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Auto-approval blocked by 19 unresolved issues from previous reviews.
Re-trigger cubic
The Pro check matched on plan id alone, and the annotation on the callback hid the rest of the subscription from inference. Autumn returns scheduled plans in the same array as active ones and the status enum is open, so a plan that hadn't started could show the Pro badge and replace the upgrade button with Manage billing. Unreachable with two plans, where the only scheduled state is a downgrade to free while pro stays active. Worth pinning before an add-on or a third plan makes it reachable.
resolveWorkspaceAiMessageAccess delegates to getWorkspaceAiFallbackModelId, so asserting the two agree compared the function to itself. The two direct mapping tests are what actually pin the contract. Also trims two comments that had grown longer than the code they explain.
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Requires human review: Auto-approval blocked by 18 unresolved issues from previous reviews.
Re-trigger cubic
A Pro subscriber who exhausted premium was told to get 400 messages with Pro, in both the model picker and the composer notice. Subscribers now get the fact without the pitch, from one shared plan check rather than a third copy of the subscription matching. The composer also stops recomputing the allowance decision. It restated the server's matrix in its own booleans, which is what let it promise one model while the gate picked another; it now runs resolveWorkspaceAiMessageAccess and reports the model that came back. That leaves the fallback mapping with a single caller, so it is no longer exported. Corrects the catalog comment claiming allowances are metered in credits — every message and upload decrements by one — and sources the page-count figures behind per-upload metering.
Upload blocks were already queryable through the rejected intake event, but an AI block only existed as a thrown error, so counting them meant string matching "Usage limit reached" inside ai_turn_failed — fragile, and it files a billing wall next to crashes. Emitted at the two check functions rather than their callers, so it fires wherever the gate is consulted, and only when someone is genuinely stopped: falling back to the other tier is not a wall and should not read as one. This is the number that says whether the premium top-up in the catalog is worth wiring into the UI, and for free or paid users.
The fallback model is hardcoded per tier. All three premium models cost the same today, so the pick is free — but gateway prices move, and if Sonnet drifts above Terra nothing would notice every fallback landing on the most expensive model in the tier. A check rather than logic: deriving the cheapest model would let array order break a three-way tie, so a catalog reshuffle would silently move the fallback. Verified it fails by raising Sonnet's cost.
There was a problem hiding this comment.
2 issues found across 11 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/features/account/use-pro-plan.ts">
<violation number="1" location="src/features/account/use-pro-plan.ts:15">
P2: Pro subscribers briefly see upgrade prompts while the customer query loads: this hook returns `false` for both an actual Free customer and unknown/loading state, and the composer/model-picker callers render the pitch for `false`. Expose loading/unknown state (or have callers gate the prompt on `isLoading`) so the prompt waits for a resolved customer.</violation>
</file>
<file name="src/integrations/autumn/workspace-ai-usage.ts">
<violation number="1" location="src/integrations/autumn/workspace-ai-usage.ts:12">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
The new blocked-path behavior in `checkWorkspaceAiMessageAccess` — firing a `usage_limit_reached` PostHog event when both tiers are exhausted — is not covered by any test in this PR. The 6 existing unit tests only exercise the pure `resolveWorkspaceAiMessageAccess` decision function in `workspace-ai-access.test.ts`, not this wired integration path. A practical regression test could mock `getAutumnClient` and `capturePostHogServerEvent`, exhaust both tiers, and assert the event fires with the expected payload. The PR's own 'Known gaps' section acknowledges this wiring is untested.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| * `subscriptions` also carries scheduled plans, and the status enum is open, so | ||
| * only an explicitly active Pro counts. | ||
| */ | ||
| export function useIsProPlan() { |
There was a problem hiding this comment.
P2: Pro subscribers briefly see upgrade prompts while the customer query loads: this hook returns false for both an actual Free customer and unknown/loading state, and the composer/model-picker callers render the pitch for false. Expose loading/unknown state (or have callers gate the prompt on isLoading) so the prompt waits for a resolved customer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/account/use-pro-plan.ts, line 15:
<comment>Pro subscribers briefly see upgrade prompts while the customer query loads: this hook returns `false` for both an actual Free customer and unknown/loading state, and the composer/model-picker callers render the pitch for `false`. Expose loading/unknown state (or have callers gate the prompt on `isLoading`) so the prompt waits for a resolved customer.</comment>
<file context>
@@ -0,0 +1,23 @@
+ * `subscriptions` also carries scheduled plans, and the status enum is open, so
+ * only an explicitly active Pro counts.
+ */
+export function useIsProPlan() {
+ const { data: customer } = useCustomer();
+
</file context>
| } from "#/integrations/autumn/workspace-ai-access"; | ||
| import { getAutumnClient, trackAutumnUsage } from "#/integrations/autumn/client.server"; | ||
| import { recordOperationalFailure } from "#/integrations/observability/operational-events"; | ||
| import { capturePostHogServerEvent } from "#/integrations/posthog/server"; |
There was a problem hiding this comment.
P2: Custom agent: Flag AI Slop and Fabricated Changes
The new blocked-path behavior in checkWorkspaceAiMessageAccess — firing a usage_limit_reached PostHog event when both tiers are exhausted — is not covered by any test in this PR. The 6 existing unit tests only exercise the pure resolveWorkspaceAiMessageAccess decision function in workspace-ai-access.test.ts, not this wired integration path. A practical regression test could mock getAutumnClient and capturePostHogServerEvent, exhaust both tiers, and assert the event fires with the expected payload. The PR's own 'Known gaps' section acknowledges this wiring is untested.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/integrations/autumn/workspace-ai-usage.ts, line 12:
<comment>The new blocked-path behavior in `checkWorkspaceAiMessageAccess` — firing a `usage_limit_reached` PostHog event when both tiers are exhausted — is not covered by any test in this PR. The 6 existing unit tests only exercise the pure `resolveWorkspaceAiMessageAccess` decision function in `workspace-ai-access.test.ts`, not this wired integration path. A practical regression test could mock `getAutumnClient` and `capturePostHogServerEvent`, exhaust both tiers, and assert the event fires with the expected payload. The PR's own 'Known gaps' section acknowledges this wiring is untested.</comment>
<file context>
@@ -9,6 +9,7 @@ import {
} from "#/integrations/autumn/workspace-ai-access";
import { getAutumnClient, trackAutumnUsage } from "#/integrations/autumn/client.server";
import { recordOperationalFailure } from "#/integrations/observability/operational-events";
+import { capturePostHogServerEvent } from "#/integrations/posthog/server";
export interface TrackWorkspaceAiMessageUsageInput {
</file context>
Adds usage plans, metering, and enforcement, plus the settings and pricing UI that surfaces them.
What's in it
Plans —
autumn.config.tsis now the source of truth for the catalog. Free: 500 standard messages, 30 premium, 50 file uploads per month. Pro at $7.99/mo: 3,000 / 400 / 500. A prepaid premium-credits add-on at $8 per 100.Metering — three metered features tracked through Autumn:
standard_messages,premium_messages,file_uploads. Named "file uploads" rather than "documents" because Document is already a workspace item type.Enforcement —
checkWorkspaceAiMessageAccessruns once per turn before the model call. If the chosen tier is spent it falls back to the other tier (premium →auto, standard →claude-sonnet) rather than to a sibling model; only when both are spent does it block. It fails open on any Autumn error, so a billing outage never takes down chat. The pure decision function is inworkspace-ai-access.tswith 6 unit tests. Uploads are gated the same way, with a toast on refusal.Disclosure —
AiChatAllowanceNoticesits in the composer header and has exactly two states: "will use a cheaper model" and "blocked until ". No low-balance countdown — premium exhaustion degrades rather than walls, so warning about it manufactures dread.Settings — moved from a page to a dialog driven by
?settings=account|plan, so upgrade prompts anywhere in the app are linkable. The old route redirects.Model costs —
models.tsweights were buckets (1–4) that didn't match their own stated derivation. Now real multipliers from Vercel AI Gateway prices (auto 1 … gemini-pro 10). Internal only, never rendered.Telemetry fixes — extraction and intake were minting a new PostHog person per workflow run via
distinctId: actorUserId ?? instanceId(247 phantom uploaders vs 25 real). Fixed at the sharedcapturePostHogServerEventwithprocessPerson, which covers both call sites. Extraction now reportscredits_used, whichprovider_modealone couldn't tell you.Deploy guard — top-level
wrangler.jsoncnamewasthinkex, colliding with the production env. A barewrangler deploywould have overwritten the live worker with staging R2/Vectorize while keeping production D1. Renamed tothinkex-dev.Read this before merging
The Autumn production catalog and Stripe are already live. The plans were pushed and payment was verified end to end —
previewAttachonproin production returnssubtotal: 7.99, total: 7.99, usd, a real Autumn → Stripe round trip. Nothing is deployed yet, so production currently runs old code that finds no key and bills nothing. Merging and deploying activates billing for everyone immediately, including the free-tier caps.Production reads
AUTUMN_PROD_SECRET_KEY; dev readsAUTUMN_SECRET_KEY. The split is by variable name, notimport.meta.env.PROD— Vite sets that for staging builds too, which would send staging traffic into Autumn production.Known gaps
attacha customisedfreeplan with tiny allowances to one throwaway customer.probe_payment_readinesscustomer is left in Autumn production from the payment check; worth deleting.atmn plansrenders Pro as$0.08/month. The stored amount is correct (7.99,display.primary_text: "$7.99") — it's a CLI display bug dividing by 100, in both environments. Reported separately.The bottom four commits are unrelated
14fd4479(MIT relicense),42d826ab(wrangler 4.118.0),494ccc2c(dep updates),29d68c42(security advisories) were already on localmainbefore this work started. They're housekeeping, not part of this change. Happy to split them into their own PR if you'd rather review them separately.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit