Skip to content

ORB-187 Make AI message quota daily - #492

Merged
thomasluizon merged 6 commits into
mainfrom
feature/ticket-193-daily-ai-quota
Aug 23, 2026
Merged

ORB-187 Make AI message quota daily#492
thomasluizon merged 6 commits into
mainfrom
feature/ticket-193-daily-ai-quota

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Implements ORB-187.

Summary

  • Changes Astra quotas to 5 messages per local day for free accounts and 50 for Pro access, including the 7 day trial.
  • Resolves the user's local date through IUserDateService inside the atomic quota consumer, with UTC fallback when no timezone is set.
  • Removes AdRewardBonusMessages from both limit calculations while leaving the rewarded ad contract in place for ORB-194.
  • Adds one reversible migration that renames the counter, replaces the rolling reset timestamp with a local date, zeros existing counters, and swaps the monthly config rows for daily rows.
  • Updates Astra and MCP wording and appends free cap hits, Pro cap hits, mean usage, and p95 usage to the existing daily AI summary log.

Contract and rollout

The profile and subscription DTO fields remain period neutral. The only contract surface removed is the raw FreeAiMessagesPerMonth config key. A read of orbit-ui-mobile main confirmed the web hook casts res.json() to AppConfig, the mobile hook requests apiClient<AppConfig>, and neither hook invokes appConfigSchema, so the missing key does not cause runtime validation. No client repository change or minimum version gate is required.

Between this deployment and ORB-193, shipped Android clients can still show the rewarded ad button even though its claim no longer changes the usable limit. The ticket's production read from 2026-08-05 found all 78 accounts at a zero bonus, a maximum bonus of zero, and one account that had ever claimed a reward, so this is a known low exposure interval.

Both limits remain database config rows and take effect through the existing cache. Deploy on a weekday morning so the first local midnight rollovers can be observed.

Testing

  • dotnet build Orbit.slnx: 0 errors
  • dotnet test --no-build: 5,956 passed
  • Focused coverage includes fifth allowed and sixth refused, next local day refill, Auckland and Sao Paulo rollover independence, UTC fallback, Pro at 50, trial at the Pro rate, ad bonus exclusion, smoke account bypass, optimistic concurrency, profile limits, stale usage reads after local midnight, backward and repeated timezone date transitions, MCP wording, daily metrics, and migration operations.
  • Forward and reverse EF migration scripts were generated and inspected for the renamed columns, counter reset, local date, and daily and monthly config rows.
  • node tools/check-dashes.mjs --files <changed files> passed.

Assumptions

  • ORB-91 already centralizes the four AI consumption paths in PayGateService, so the local date is resolved once inside that service; I rejected duplicating date resolution across four handlers and weakening the atomic boundary.
  • The acceptance grep applies to live source, excluding immutable historical migrations and this migration's Down; I rejected editing the applied 2026-03-19 seed because the ticket explicitly forbids it and rollback must restore monthly names.
  • Daily quota metrics include active users whose stored local counter date equals the summarized date; I rejected adding a durable history table because the ticket specifies metrics from User rows alone.

External interface evidence

The migration test reads EF operation properties InsertDataOperation.Values and DeleteDataOperation.KeyValues. The installed Microsoft.EntityFrameworkCore.Relational 10.0.10 XML reference documents both properties. Re-derive after dotnet restore from %USERPROFILE%/.nuget/packages/microsoft.entityframeworkcore.relational/10.0.10/lib/net10.0/Microsoft.EntityFrameworkCore.Relational.xml.

@thomasluizon

Copy link
Copy Markdown
Owner Author

Intended approach

  1. Replace the rolling monthly timestamp with a nullable local date on User, preserve the period neutral API fields, and add one EF migration that adds the date, resets every counter, replaces the two config rows, and restores the monthly model in Down.
  2. Resolve each user's date through IUserDateService at all four increment paths. This uses the existing timezone service, including its UTC fallback, instead of deriving a calendar date from a UTC timestamp.
  3. Rename the config keys to daily, set defaults to 5 and 50, remove the ad bonus from both pay gate calculations, and update the API and Astra wording that describes the quota.
  4. Add focused domain, pay gate, handler, timezone, profile, and migration coverage for limits, rollover, independent timezones, UTC fallback, ad bonus exclusion, smoke bypass, and reversible schema behavior.

I am keeping the period neutral DTOs and the rewarded ad fields and endpoints unchanged. I rejected a rolling 24 hour timestamp because the ticket requires local midnight, and I rejected broader ad removal or client schema edits because those belong to separate tickets.

pullfrog[bot]
pullfrog Bot previously approved these changes Aug 23, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes at 27bc5b0, this PR contains only branch initialization and has no file diff.

  • Initialization commit: chore: start ORB-187 leaves the tree identical to the base; the quota implementation described in the PR body has not landed yet.

Pullfrog  | View workflow run | Using GPT Sol𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

The daily quota has two rollover paths that can either deny a valid allowance or remove the cap entirely.

Reviewed changes since the prior Pullfrog review at 27bc5b0; this run covered the complete implementation added at 3dbc329.

  • Daily quota enforcement: Replaced monthly limits with five free and 50 Pro messages per user-local day while retaining atomic optimistic-concurrency consumption.
  • Local-date persistence: Replaced the rolling reset timestamp with a local-date bucket, reset existing counters through a reversible migration, and changed the database config rows to daily limits.
  • Client and agent surfaces: Kept period-neutral profile and subscription fields, updated Astra and MCP wording, and removed ad reward bonuses from usable limits.
  • Operational reporting: Added free and Pro cap hits, mean usage, and p95 usage to the existing daily AI summary log.
  • Unit coverage: Added forward-rollover, timezone-boundary, plan-limit, concurrency, migration-shape, and quota-metric tests.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT Sol𝕏

Comment thread src/Orbit.Application/Profile/Queries/GetProfileQuery.cs Outdated
Comment thread src/Orbit.Domain/Entities/User.cs
@thomasluizon

Copy link
Copy Markdown
Owner Author

Addressed both rollover findings in ebbb175. Profile, subscription, and MCP reads now report zero when the stored quota date differs from the current local date. Consumption starts a bounded bucket for every supported date transition, including backward movement after timezone travel. Regression coverage exercises a capped prior day read and repeated consumption after moving to an earlier local date. Full build passes with 0 errors and all 5,954 tests pass.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

The rollover fixes close the prior defects, but the new mismatch reset still allows the daily quota to be refilled repeatedly through timezone changes.

Reviewed changes since Pullfrog's prior coverage at 31385c5; this run reviewed the behavioral fixes in ebbb175.

  • Normalized displayed usage: Projected mismatched stored quota dates as zero across profile, subscription, and MCP surfaces.
  • Changed date transition handling: Reset the quota bucket on any local date mismatch and added backward transition tests.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT Sol𝕏

Comment thread src/Orbit.Domain/Entities/User.cs Outdated
@thomasluizon

Copy link
Copy Markdown
Owner Author

Addressed the repeated timezone transition finding in 7cb6d15. The persisted quota date now advances monotonically, and enforcement carries the active count when the resolved local date moves backward. A regression exhausts date A, exhausts date B, returns to date A, and confirms no third allowance is created. Full build passes with 0 errors and all 5,956 tests pass.

pullfrog[bot]
pullfrog Bot previously approved these changes Aug 23, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes since the prior Pullfrog review at ebbb175; this run covered the timezone quota hardening in 7cb6d15.

  • Hardened quota enforcement: Counted the newest stored bucket when the resolved local date moves backward, preventing a date mismatch from bypassing the limit.
  • Made bucket transitions monotonic: Opened a fresh allowance only for a strictly newer local date while preserving the newest bucket across backward timezone travel.
  • Added regression coverage: Verified that backward and alternating local-date sequences cannot mint a third allowance while normal next-day rollover still refills the quota.

Pullfrog  | View workflow run | Using GPT Sol𝕏

…ily-ai-quota

# Conflicts:
#	architecture.html
#	architecture.json
#	src/Orbit.Application/Chat/Content/FeatureExplanations/paygate.md
#	src/Orbit.Application/Common/AppConstants.cs
@thomasluizon

Copy link
Copy Markdown
Owner Author

Merged current main into this branch and resolved four conflicts, all from #490 and #491 landing first:

Verified locally on the merged tree before pushing: build 0 errors, full suite 5,955 passed / 0 failed.

@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes since the prior Pullfrog review at 7cb6d15; this run covered the merged main integration in 2e2649f.

  • Integrated the habit ceiling: Preserved the 1000 live top-level habit abuse guard in the conflict-resolved constants, paygate explanation, and test setup without changing the daily AI quota defaults.
  • Integrated free goal creation: Removed goals from the Pro unlock list and retained Pro gating only for AI goal reviews, including the corresponding test setup changes.
  • Refreshed architecture artifacts: Combined the new base branch coverage counts and references with the daily quota architecture changes.

Pullfrog  | View workflow run | Using GPT Sol𝕏

@sonarqubecloud

Copy link
Copy Markdown

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

The server's effective quota can disagree with every reported usage surface after backward timezone travel, and the new quota summary cannot produce complete local-day metrics from its UTC snapshot.

Reviewed changes at 2e2649f; this run reviewed the complete daily quota implementation after its merge from main.

  • Daily quota enforcement: Replaced monthly limits with five free and 50 Pro messages per user-local day while preserving optimistic-concurrency consumption and smoke-account bypass.
  • Timezone-aware state: Replaced the rolling reset timestamp with a local-date bucket, made transitions monotonic against repeated timezone changes, and projected lazy resets into profile, subscription, and MCP responses.
  • Persistence and configuration: Added a reversible migration that resets existing counters and swaps monthly configuration rows for daily limits.
  • Client and operational surfaces: Kept period-neutral profile fields, removed ad rewards from effective limits, updated Astra wording, and added cap-hit, mean, and p95 metrics with unit coverage.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT Sol𝕏

}

public int GetAiMessagesUsedToday(DateOnly userToday) =>
AiMessagesLocalDate == userToday ? AiMessagesUsedToday : 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When the resolved date moves backward, GetAiMessagesUsedForQuota preserves the future bucket for enforcement, but this method reports zero through profile, subscription, and MCP. Both shipped chat composers then advertise available quota and submit a request that the server rejects until the local date catches up.

Technical details
# Keep reported usage aligned with effective enforcement

## Affected sites
- `src/Orbit.Domain/Entities/User.cs:356-360`: display usage requires exact date equality while enforcement counts a stored future bucket.
- `src/Orbit.Application/Profile/Queries/GetProfileQuery.cs:121`: exposes zero even when `TryConsumeAiMessage` will reject at the preserved limit.
- `src/Orbit.Application/Subscriptions/Queries/GetSubscriptionStatusQuery.cs:30`: exposes the same inconsistent count.
- `src/Orbit.Api/Mcp/Tools/SubscriptionTools.cs:48`: labels the inconsistent value as daily usage.
- `orbit-ui-mobile/apps/mobile/hooks/use-chat-composer.ts:153-155`: derives the local free-plan block from the reported count.
- `orbit-ui-mobile/apps/web/hooks/use-chat-composer.ts:177-179`: derives the same local block from the reported count.

## Required outcome
- Report the same effective usage that quota enforcement applies when the stored bucket is in the future, while still reporting zero for an expired previous-day bucket after normal midnight rollover.
- Cover a profile or subscription read after a backward local-date transition with the preserved bucket already at its limit.

## Suggested approach
- Reuse the effective quota projection for response surfaces instead of maintaining exact-match and monotonic projections with different behavior.


var activeUsers = await dbContext.Users
.AsNoTracking()
.Where(user => user.AiMessagesLocalDate == yesterday && user.AiMessagesUsedToday > 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yesterday is a UTC date and the job runs at 00:05 UTC, but this mutable bucket follows each user's local date. At that instant west-offset users are counted before their day ends, while east-offset users who sent after local midnight have already overwritten the prior bucket, so cap hits, mean, and p95 are partial or omitted.

Technical details
# Finalize quota metrics against completed local days

## Affected sites
- `src/Orbit.Infrastructure/Services/AiUsageSummaryService.cs:60`: chooses the previous UTC date.
- `src/Orbit.Infrastructure/Services/AiUsageSummaryService.cs:73-76`: reads only the user's current mutable local-date bucket.
- `src/Orbit.Infrastructure/Services/AiUsageSummaryService.cs:143-150`: calculates cap hits, mean, and p95 from that incomplete population.
- `tests/Orbit.Infrastructure.Tests/Services/AiUsageSummaryServiceGenerationTests.cs:63-71`: seeds all users directly into the UTC `Yesterday` bucket, so no offset can expose the loss.

## Required outcome
- Build each reported local-day metric from complete usage for that finished user-local date, without losing a bucket when the next local day starts.
- Add boundary coverage showing a west-offset user's still-open day and an east-offset user's already-advanced bucket at 00:05 UTC.

## Suggested approach
- Persist or otherwise finalize usage per user-local date before aggregating it into the single daily summary; one mutable current bucket cannot represent both days after rollover.

@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog

pullfrog Bot commented Aug 23, 2026

Copy link
Copy Markdown
  • Check out PR 492 and read the complete authoritative diff
  • Trace changed behavior, contracts, and tests through both repositories
  • Validate any disposition-changing risks and submit one review

Pullfrog  | View workflow run | via Pullfrog | Using GPT Sol𝕏

@thomasluizon
thomasluizon merged commit 5aa9823 into main Aug 23, 2026
22 of 23 checks passed
@thomasluizon
thomasluizon deleted the feature/ticket-193-daily-ai-quota branch August 23, 2026 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant