From 37183ae75ef392a548c4ebe1ad25d68f3a9374cd Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Thu, 25 Jun 2026 11:44:24 +0200 Subject: [PATCH 01/11] feat(cost-insights) planning --- .plans/cost-insights-data-layer.md | 661 ++++++++++++++++++ .plans/cost-insights.md | 149 ++++ .specs/cost-insights.md | 236 +++++++ AGENTS.md | 1 + CONTEXT.md | 113 ++- apps/storybook/stories/Sidebar.stories.tsx | 6 + .../stories/cost-insights/AskKilo.stories.tsx | 24 + .../CostInsightsAlertBar.stories.tsx | 31 + .../cost-insights/EventHistory.stories.tsx | 76 ++ .../cost-insights/Overview.stories.tsx | 150 ++++ .../cost-insights/Settings.stories.tsx | 85 +++ .../stories/cost-insights/Shell.stories.tsx | 21 + .../cost-insights/costInsightsFixtures.ts | 475 +++++++++++++ .../components/OrganizationAppSidebar.tsx | 14 +- .../(app)/components/PersonalAppSidebar.tsx | 6 + .../activity/CostInsightsEventHistoryView.tsx | 153 ++++ .../cost-insights/activity/EventList.tsx | 168 +++++ .../ask-kilo/CostInsightsAskKiloView.tsx | 177 +++++ .../components/cost-insights/formatting.ts | 29 + .../web/src/components/cost-insights/index.ts | 25 + .../cost-insights/overview/AskKiloInput.tsx | 59 ++ .../overview/CostInsightsDashboardView.tsx | 114 +++ .../overview/DashboardNotices.tsx | 160 +++++ .../overview/EventPreviewCard.tsx | 25 + .../overview/SpendEvidenceCard.tsx | 219 ++++++ .../cost-insights/overview/TopDriversCard.tsx | 127 ++++ .../settings/CostInsightsSettingsView.tsx | 189 +++++ .../shared/CostInsightsLoadError.tsx | 23 + .../cost-insights/shared/EmptyPanel.tsx | 8 + .../cost-insights/shared/StatusBadge.tsx | 25 + .../shell/CostInsightsAlertBar.tsx | 39 ++ .../shell/CostInsightsShellView.tsx | 149 ++++ .../web/src/components/cost-insights/types.ts | 122 ++++ 33 files changed, 3856 insertions(+), 3 deletions(-) create mode 100644 .plans/cost-insights-data-layer.md create mode 100644 .plans/cost-insights.md create mode 100644 .specs/cost-insights.md create mode 100644 apps/storybook/stories/cost-insights/AskKilo.stories.tsx create mode 100644 apps/storybook/stories/cost-insights/CostInsightsAlertBar.stories.tsx create mode 100644 apps/storybook/stories/cost-insights/EventHistory.stories.tsx create mode 100644 apps/storybook/stories/cost-insights/Overview.stories.tsx create mode 100644 apps/storybook/stories/cost-insights/Settings.stories.tsx create mode 100644 apps/storybook/stories/cost-insights/Shell.stories.tsx create mode 100644 apps/storybook/stories/cost-insights/costInsightsFixtures.ts create mode 100644 apps/web/src/components/cost-insights/activity/CostInsightsEventHistoryView.tsx create mode 100644 apps/web/src/components/cost-insights/activity/EventList.tsx create mode 100644 apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx create mode 100644 apps/web/src/components/cost-insights/formatting.ts create mode 100644 apps/web/src/components/cost-insights/index.ts create mode 100644 apps/web/src/components/cost-insights/overview/AskKiloInput.tsx create mode 100644 apps/web/src/components/cost-insights/overview/CostInsightsDashboardView.tsx create mode 100644 apps/web/src/components/cost-insights/overview/DashboardNotices.tsx create mode 100644 apps/web/src/components/cost-insights/overview/EventPreviewCard.tsx create mode 100644 apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx create mode 100644 apps/web/src/components/cost-insights/overview/TopDriversCard.tsx create mode 100644 apps/web/src/components/cost-insights/settings/CostInsightsSettingsView.tsx create mode 100644 apps/web/src/components/cost-insights/shared/CostInsightsLoadError.tsx create mode 100644 apps/web/src/components/cost-insights/shared/EmptyPanel.tsx create mode 100644 apps/web/src/components/cost-insights/shared/StatusBadge.tsx create mode 100644 apps/web/src/components/cost-insights/shell/CostInsightsAlertBar.tsx create mode 100644 apps/web/src/components/cost-insights/shell/CostInsightsShellView.tsx create mode 100644 apps/web/src/components/cost-insights/types.ts diff --git a/.plans/cost-insights-data-layer.md b/.plans/cost-insights-data-layer.md new file mode 100644 index 0000000000..8470955c07 --- /dev/null +++ b/.plans/cost-insights-data-layer.md @@ -0,0 +1,661 @@ +# Cost Insights data layer implementation plan + +## Status + +Ready for implementation. This plan covers Credit-spend capture, owner-hour rollups, read repositories, historical backfill, repair, and rollout validation. Alert evaluation, Cost Insight Events, notifications, tRPC routes, and UI are follow-on work. + +The business rules remain in `.specs/cost-insights.md`. Canonical terminology remains in `CONTEXT.md`. The broader feature sequence remains in `.plans/cost-insights.md`. + +## Goal + +Create a Postgres data source that can answer, for one Spend owner: + +- Variable and Scheduled Credit spend by UTC hour. +- Current-hour Variable Credit spend. +- Zero-filled hourly evidence for 24h, 7d, 30d, and 90d ranges. +- Top spend drivers by source, product or feature, model or plan, provider, and actor user. +- Whether a requested historical range is complete enough to treat missing rows as zero. + +Every production operation that increments `microdollars_used` must update this data source in the same database transaction. Snowflake is not part of capture, repair, or correctness. + +## Non-goals + +- Do not build anomaly or Spend Threshold Alert evaluation yet. +- Do not create Cost Insight Events, notification delivery rows, settings, or owner alert state. +- Do not change current Credit pricing, admission, low-balance behavior, auto-top-up, or Kilo Pass behavior. +- Do not replace `microdollar_usage_daily`; its Kilo Pass consumer remains unchanged. +- Do not make all-time historical backfill a launch gate. Bootstrap 90 days, retain those rows and all new rows indefinitely, then expand older history only if product needs exceed the v1 90-day evidence window. +- Do not add user email, display name, request prompt, project, session, instance name, or arbitrary client metadata to rollups. + +## Decisions + +### Storage shape + +Add four unpartitioned tables: + +1. `cost_insight_owner_hour_totals` +2. `cost_insight_owner_hour_driver_buckets` +3. `cost_insight_rollup_coverage` +4. `cost_insight_rollup_degraded_intervals` + +The first two are sparse. Zero-spend hours do not create rows. Coverage metadata and unresolved degraded intervals tell readers when a missing row is a trustworthy zero. + +Do not add a second per-spend contribution ledger. Existing source rows and idempotent billing records remain the contribution ledger. Duplicating every AI Gateway request would defeat the purpose of compact rollups. + +### No partitioning in the first version + +The current daily table is not partitioned, despite the conversation suggesting it as precedent. Hourly totals and driver buckets are compact aggregate tables with conflict updates, not append-only request logs. Partitioning would add partition-provisioning failure modes to mandatory Credit-spend writes and cannot be generated cleanly through the current Drizzle schema workflow. + +Ship unpartitioned tables, measure row growth, index size, autovacuum behavior, update latency, and owner-hour lock contention. Revisit monthly `hour_start` partitioning only when measured table maintenance or index size requires it. Missing future partitions must never become a reason Credit spend fails. + +### Owner identity + +Use the established exactly-one-owner pattern: + +- `owned_by_user_id text NULL` +- `owned_by_organization_id uuid NULL` +- Check constraint requiring exactly one value. +- Separate partial unique indexes for personal and organization owners. + +Do not use a polymorphic `(owner_type, owner_id)` pair. Typed foreign keys and partial indexes keep owner integrity and query plans explicit. + +### Spend classification + +Persist two controlled categories: + +- `variable` +- `scheduled` + +Persist four controlled sources from the Cost Insights spec: + +- `ai_gateway` +- `kiloclaw` +- `coding_plan` +- `other` + +Exa uses `other` in v1 because `exa` is not in the approved source taxonomy. Its `product_key` remains `exa`, so the UI can still identify it. + +### Consistency + +Live capture is additive and transaction-bound. Backfill and repair are absolute replacements computed from canonical Postgres sources. + +The shared capture helper never opens or commits a transaction. Callers pass their current transaction. A source row, owner balance mutation, owner-hour total, and driver bucket either commit together or roll back together. + +### Timestamps + +Use the source spend timestamp, not processing or backfill time. Normalize buckets with explicit UTC SQL, for example `date_trunc('hour', occurred_at, 'UTC')`. Do not rely on database session timezone. + +## Data model + +### Controlled schema values + +Add `CostInsightSpendCategory` and `CostInsightSpendSource` runtime/type values to `packages/db/src/schema-types.ts`. Register them in `SCHEMA_CHECK_ENUMS` and enforce them through `enumCheck` constraints in `packages/db/src/schema.ts`. + +### Owner-hour totals + +`cost_insight_owner_hour_totals` stores the amount used by charts and alert evaluation. + +| Column | Contract | +|---|---| +| `id` | UUID primary key | +| `owned_by_user_id` | Nullable FK to `kilocode_users.id` | +| `owned_by_organization_id` | Nullable FK to `organizations.id` | +| `hour_start` | UTC-truncated `timestamptz` | +| `spend_category` | `variable` or `scheduled` | +| `total_microdollars` | Positive `bigint`, Drizzle number mode | +| `spend_record_count` | Positive `bigint`, one per contributing source spend record | +| `created_at` | Insert timestamp | +| `updated_at` | Explicitly updated by conflict updates | + +Constraints and indexes: + +- Exactly one owner check. +- UTC-hour check. +- Positive amount and record-count checks. +- Personal partial unique index on `(owned_by_user_id, hour_start, spend_category)` where organization is null. +- Organization partial unique index on `(owned_by_organization_id, hour_start, spend_category)` where user is null. + +Index order keeps an owner's 24h through 90d rows contiguous. At most two total rows exist per owner-hour. + +### Driver buckets + +`cost_insight_owner_hour_driver_buckets` stores compact attribution. Each captured spend record contributes to one combined driver bucket, not one row per dimension. + +| Column | Contract | +|---|---| +| `id` | UUID primary key | +| `owned_by_user_id` | Nullable owner FK | +| `owned_by_organization_id` | Nullable owner FK | +| `hour_start` | Same UTC bucket as total row | +| `spend_category` | Controlled category | +| `driver_key` | SHA-256 digest of normalized source, dimensions, and actor identity | +| `source` | Controlled source | +| `product_key` | Non-null controlled product key or `other` | +| `feature_key` | Non-null controlled operation/feature key or `other` | +| `model_or_plan_key` | Non-null existing model/plan identifier or `other` | +| `provider_key` | Non-null existing provider identifier or `other` | +| `actor_user_id` | FK to charged/attributed user; required for personal and organization spend | +| `total_microdollars` | Positive `bigint` | +| `spend_record_count` | Positive `bigint` | +| `created_at` | Insert timestamp | +| `updated_at` | Explicitly updated by conflict updates | + +Use non-null `other` sentinels for unavailable dimensions. This prevents null uniqueness from creating duplicate buckets and keeps grouping simple. + +Compute `driver_key` in the shared package from a fixed v1, length-prefixed serialization of source, all four driver dimensions, and actor user ID. Store a 32-byte/64-hex SHA-256 digest. This keeps the mandatory conflict index narrow while retaining readable dimension columns. + +The v1 serialization and normalization contract is immutable. Any later mapping/key change must increment the global `rollup_version`, delete and rebuild affected driver rows from canonical sources, and re-establish coverage; it must not mix key versions in one covered interval. Bulk backfill deletes every driver row in its target hour before insertion. Targeted repair deletes every driver row for its owner-hour before insertion. Reruns therefore cannot leave parallel old-key buckets. + +Create separate personal and organization partial unique indexes on owner, hour, category, and `driver_key`. On live-write conflict, update only when stored dimensions exactly match the incoming dimensions; a digest/dimension mismatch throws and rolls back rather than merging unrelated buckets. The owner/hour prefix also supports range scans. Benchmark top-driver reads before adding another range index. + +Driver values must be bounded identifiers, not labels or arbitrary request strings. Normalize empty or unsupported values to `other` and cap identifier lengths before persistence. + +### Coverage + +`cost_insight_rollup_coverage` has one low-write global row for the current rollup format. + +| Column | Contract | +|---|---| +| `rollup_version` | Small integer primary key; v1 is `1` | +| `live_capture_start_hour` | First UTC hour after all production spend writers had mandatory capture | +| `coverage_start_hour` | Earliest UTC hour rebuilt and verified across all canonical sources | +| `last_reconciled_at` | Last successful canonical-source reconciliation | +| `created_at` | Insert timestamp | +| `updated_at` | Explicit update timestamp | + +`cost_insight_rollup_degraded_intervals` records known exceptions to the global claim. + +| Column | Contract | +|---|---| +| `id` | UUID primary key | +| `start_hour` | Inclusive UTC hour | +| `end_hour_exclusive` | Exclusive UTC hour | +| `source` | Optional controlled source; null means all sources | +| `reason` | Controlled operational reason, not arbitrary error text | +| `detected_at` | Detection timestamp | +| `resolved_at` | Null until repair and reconciliation complete | +| `created_at` | Insert timestamp | +| `updated_at` | Explicit update timestamp | + +Coverage is global because bootstrap scans every canonical source for each covered hour. A missing owner total is a trustworthy zero only when the hour is at or after `coverage_start_hour` and no unresolved degraded interval overlaps it. Before coverage begins, or inside a degraded interval, it is unknown. + +Do not advance `coverage_start_hour` across an unprocessed hour. The backfill job moves it backward one contiguous completed hour at a time. Atomic live capture makes coverage continuous after `live_capture_start_hour`. + +Create a degraded interval before an intentional capture bypass, and immediately when reconciliation detects a possible gap. Repair and reconcile the full interval before setting `resolved_at`. This preserves honest zero-fill semantics without creating per-owner coverage rows. + +## Shared capture module + +Add a subpath-only export `@kilocode/db/cost-insights-rollups`, backed by `packages/db/src/cost-insights-rollups.ts` and exported through `packages/db/package.json`. Do not add it to the broad root barrel; explicit imports keep the billing-critical persistence boundary visible in web and Worker call sites. + +The module owns: + +- Spend owner, category, source, and driver input types. +- Input validation. +- Explicit UTC bucket calculation. +- Transaction-scoped owner-hour advisory locking. +- Total and driver additive upserts in a fixed lock order. +- Generic owner-range read helpers that do not format USD or resolve labels. + +Suggested input: + +```ts +type CostInsightSpendOwner = + | { type: 'user'; id: string } + | { type: 'organization'; id: string }; + +type CaptureCostInsightSpendInput = { + owner: CostInsightSpendOwner; + actorUserId: string; + occurredAt: string; + amountMicrodollars: number; + spendRecordCount?: number; + category: CostInsightSpendCategory; + source: CostInsightSpendSource; + productKey: string; + featureKey: string; + modelOrPlanKey: string; + providerKey: string; +}; +``` + +`captureCostInsightSpend(tx, input)` must: + +1. Reject invalid timestamps, unsafe integers, non-positive amounts/counts, missing owner IDs, and uncontrolled category/source values. +2. Normalize driver keys to bounded values and `other` sentinels. +3. Compute the versioned driver digest and retain normalized dimensions for collision verification. +4. Acquire a transaction-scoped advisory lock derived from owner type, owner ID, and UTC hour. +5. Upsert the owner-hour total. +6. Upsert the combined driver bucket. +7. Increment amount and count and set `updated_at = now()` explicitly. + +Every writer and targeted repair acquires the same owner-hour advisory lock. This prevents an absolute repair from overwriting a concurrent live contribution. Always lock total before driver to avoid lock-order drift. + +The module must accept a structural transaction writer type. It must not import the web database singleton, create a Worker database client, or cache transport-owning state. KiloClaw Worker continues creating request-scoped clients through `getWorkerDb`. + +### Idempotency contract + +The helper is additive, so call it only after the authoritative source insertion or billing deduction is known to be new. + +- AI Gateway and Exa source inserts are new UUID records. +- Coding Plan activation and renewal use their existing term/idempotency records. +- KiloClaw enrollment and renewal call capture only when the period-scoped credit deduction insert succeeds. +- Duplicate, free, failed, refunded, expired, or balance-repair paths do not call capture. + +Rollback and retry are safe because source, balance, and rollup share one transaction. The helper does not claim to solve the existing lack of request-replay idempotency in AI Gateway or Exa. + +## Driver mapping + +| Spend path | Category | Source | Product | Feature | Model/plan | Provider | Actor | +|---|---|---|---|---|---|---|---| +| AI Gateway | `variable` | `ai_gateway` | Validated `X-KILOCODE-FEATURE` or `direct-gateway`/`other` | Validated API kind/operation or `other` | Requested model, then resolved model, then `other` | Inference provider, then gateway provider, then `other` | `kilo_user_id` | +| Charged Exa | `variable` | `other` | `exa` | Allowlisted Exa path, else `other` | `other` | `exa` | Requesting user | +| KiloClaw enrollment | `scheduled` | `kiloclaw` | `kiloclaw-hosting` | `enrollment` | `standard` or legacy `commit` | `other` | Charged user | +| KiloClaw renewal | `scheduled` | `kiloclaw` | `kiloclaw-hosting` | `renewal` | Effective plan | `other` | Charged user | +| Coding Plan activation | `scheduled` | `coding_plan` | `coding-plan` | `activation` | Plan ID | Provider ID | Charged user | +| Coding Plan renewal | `scheduled` | `coding_plan` | `coding-plan` | `renewal` | Plan ID | Provider ID | Charged user | + +Coding Plan-backed inference is BYOK/zero-cost at AI Gateway and must not create a second Variable Credit spend record. KiloClaw Stripe-funded settlement is balance-neutral bookkeeping and must not create Scheduled Credit spend. + +Every included canonical source has a stable charged or attributed user ID: `microdollar_usage.kilo_user_id`, `exa_usage_log.kilo_user_id`, `coding_plan_terms.user_id`, or `credit_transactions.kilo_user_id`. System-triggered renewals use the charged user as driver attribution; actor does not imply a human initiated the transaction. If a future Credit-spend source lacks actor user identity, it cannot silently write a null/synthetic actor; update the spec and schema contract first. + +## Spend-writer audit gate + +Before producer integration, repeat a repository-wide audit of every direct `kilocode_users.microdollars_used` and `organizations.microdollars_used` mutation. Record each production mutation in the implementation PR as included Credit spend or excluded balance/accounting mutation. + +Current direct Credit-spend set: + +| Mutation | Classification | +|---|---| +| AI Gateway personal balance update | Include: Variable `ai_gateway` | +| Organization usage helper used by AI Gateway | Include: Variable `ai_gateway` | +| Personal charged Exa balance update | Include: Variable `other`/`exa` product | +| Organization usage helper used by charged Exa | Include: Variable `other`/`exa` product | +| Coding Plan activation and renewal | Include: Scheduled `coding_plan` | +| KiloClaw pure-credit enrollment and renewal | Include: Scheduled `kiloclaw` | +| Balance recomputation | Exclude: repair canonical hours instead | +| Credit grants, purchases, auto-top-ups | Exclude: acquired credits, not spend | +| Expiration, refund, void, dispute, settlement | Exclude: accounting/acquired-credit changes | +| Admin corrections, seeds, development consume routes | Exclude from production evidence; seed rollups explicitly for local fixtures if needed | + +No writer integration is complete until this audit has no unexplained production mutation. Add a focused static/repository test or documented grep check so future `microdollars_used` mutations require an explicit Cost Insights classification. + +## Capture integrations + +### AI Gateway personal and organization spend + +Primary files: + +- `apps/web/src/lib/ai-gateway/processUsage.ts` +- `apps/web/src/lib/organizations/organization-usage.ts` + +First extract an executor-parameterized version of the existing raw multi-CTE statement without changing its one-statement behavior. Preserve the outer retry policy for current concurrency failures; each retry must start a fresh transaction and rerun source plus rollups together. Then wrap the complete personal/organization persistence path in one caller-owned transaction. + +For personal usage, keep these effects together: + +- `microdollar_usage` insert. +- Metadata and existing daily rollup. +- Personal `microdollars_used` update. +- Cost Insights total and driver upserts. + +For organization usage, remove the current second-transaction boundary. Add a transaction-aware organization spend primitive that performs: + +- Organization `microdollars_used` and legacy balance cache update. +- Organization member daily usage update. +- Cost Insights total and driver upserts. + +Split the current organization helper into a transaction-aware mutation that returns `{ crossedMinimumBalance, recipients }` and a separate post-commit scheduler. AI and Exa organization paths must use the same mutation primitive. The source row, organization charge, member daily usage, and rollups share one transaction; low-balance email is scheduled exactly once only after commit. + +Capture only positive `cost`. Zero-cost, free, and BYOK rows remain in source usage tables but are not Credit spend. + +### Exa personal and organization spend + +Primary files: + +- `apps/web/src/lib/exa-usage.ts` +- `apps/web/src/app/api/exa/[...path]/route.ts` + +Refactor `recordExaUsage` into one transaction. Precompute one source ID and timestamp, then use them for the log and rollup. + +The transaction contains: + +- `exa_usage_log` insert. +- `exa_monthly_usage` update. +- Personal or organization balance mutation when charged. +- Organization member daily usage when organization-owned. +- Cost Insights total and driver upserts when `charged_to_balance` is true and amount is positive. + +Keep free-allowance requests in the Exa source tables without Cost Insights spend. Do not insert synthetic `microdollar_usage` rows for Exa. + +### Coding Plan activation and renewal + +Primary files: + +- `apps/web/src/lib/coding-plans/index.ts` +- `apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts` + +Both paths already use transactions and idempotent term identities. Keep balance guard/update, negative credit transaction, term, and rollup in the same transaction. Capture Scheduled Credit spend only after the new deduction identity is established and before commit. If reordering the current balance update ahead of ledger insertion would complicate existing guards, retain current order and prove with failure-injection tests that any later ledger/rollup error rolls the balance update back. + +Use the same explicit `occurredAt` on the credit transaction and rollup input. Skip duplicate activation/renewal, insufficient balance, cancellation, and past-due paths. + +### KiloClaw credit enrollment and renewal + +Primary files: + +- `apps/web/src/lib/kiloclaw/credit-billing.ts` +- `services/kiloclaw-billing/src/lifecycle.ts` + +Both pure-credit charge paths already use transactions. Call capture only after a new period-scoped negative credit transaction is inserted. Capture before balance and subscription changes commit. + +Do not capture: + +- Duplicate deduction reconciliation. +- Stripe-funded settlement categories. +- Auto-top-up funding. +- Failed or deferred renewal. +- Cancellation without deduction. +- Organization-managed rows skipped by personal pure-credit renewal. + +The Worker receives the shared helper through `@kilocode/db/cost-insights-rollups` and passes its existing transaction. It must not cache database clients or pools in module scope. + +### Explicit exclusions + +Do not derive Cost Insights spend from arbitrary negative `credit_transactions`. The table also contains expirations, refunds, voids, disputes, settlement entries, and accounting adjustments. + +Do not capture direct balance recomputation deltas, admin corrections, seed scripts, or development consume endpoints as production spend. Repair rollups from canonical source records in their original hours instead. + +## Read repository + +Add `apps/web/src/lib/cost-insights/spend-repository.ts`. It should accept a database executor and typed Spend owner, with no tRPC, authorization, USD formatting, or UI labels. + +### `getOwnerHourlySpend` + +Inputs: owner, `startHour`, `endHourExclusive`. + +Return one row per UTC hour with: + +- Variable microdollars. +- Scheduled microdollars. +- Total microdollars. +- Variable and scheduled record counts. +- `isCovered`. + +Use `generate_series` and left joins so covered zero-spend hours are present. Return canonical timestamps, not display labels. Allow up to 2,160 buckets for the 90-day range. + +### `getOwnerTopSpendDrivers` + +Inputs: owner, half-open time range, optional category, limit capped at 5 for alert evidence. + +Group by the combined persisted dimensions. Sum amount and record count, order by amount descending with stable dimension tie-breakers, and limit before resolving actor labels. Actor label resolution belongs in a later application layer and uses current user rows. + +### `getOwnerCurrentHourSpend` + +Return current UTC-hour Variable and Scheduled totals from the primary database. Future post-spend evaluation must not use a lagging replica. + +### `getCostInsightRollupCoverage` + +Return live capture start, coverage start, last reconciliation, overlapping unresolved degraded intervals, and whether a requested range is fully covered. Reads must not turn uncovered or degraded missing rows into zero. + +### `getOwnerRolling24HourSpendExact` + +The spec requires an exact elapsed `[asOf - 24h, asOf)` window. Hourly totals alone cannot apportion the oldest and current partial hours. + +Add a repository helper that: + +1. Uses owner-hour totals for the fully enclosed UTC hours. +2. Uses the canonical Postgres source union for `[asOf - 24h, ceilToUtcHour(asOf - 24h))` and `[floorToUtcHour(asOf), asOf)` boundary fragments, skipping either fragment when its bounds are equal. +3. Returns Variable, Scheduled, and total microdollars under one database snapshot/as-of value. +4. Refuses to claim completeness when the interior range overlaps an unresolved degraded interval. + +This preserves exact threshold semantics without a second per-spend ledger. Benchmark the two bounded raw fragments for high-volume organizations before the threshold evaluator uses this helper after every spend; follow-on evaluation may need coalescing, but it may not replace exact rolling semantics with a 24-bucket approximation. + +## Backfill and repair + +### Canonical Postgres sources + +| Source family | Inclusion rule | +|---|---| +| AI Gateway | `microdollar_usage.cost > 0`; owner from organization when present, otherwise user; occurrence is `microdollar_usage.created_at` | +| Exa | `exa_usage_log.charged_to_balance = true` and positive cost; occurrence is `exa_usage_log.created_at` | +| Coding Plan | `coding_plan_terms` joined to its negative `credit_transactions` row; occurrence is `credit_transactions.created_at`, not term period bounds | +| KiloClaw | Pure-credit `kiloclaw-subscription:*` and `kiloclaw-subscription-commit:*` deductions only; exclude settlement; occurrence is `credit_transactions.created_at`, not renewal boundary | + +Live scheduled-spend writers set `credit_transactions.created_at` from the same explicit `occurredAt` passed to capture. Backfill, targeted repair, and exact rolling-window fragments use that field, so scheduled spend lands in the same hour under every path. + +The existing daily rollup is a secondary checksum, not a backfill source. It lacks Scheduled spend, hourly timestamps, and drivers. + +### Shared canonical mapping + +Add source-specific canonical aggregation functions under `apps/web/src/lib/cost-insights/`. Live and historical mapping must share constants for category, source, product, feature, and fallback values. + +Backfill SQL may need source-specific `CASE` expressions for set-based aggregation. Add parity fixtures proving live mapping and historical mapping produce the same driver keys for representative source rows. + +Historical gaps map to `other`; do not infer mutable event-time data from current subscriptions or user profiles. + +### Bulk backfill script + +Add an operator script under `apps/web/src/scripts/` and register it in the existing script index. It must default to dry run and require explicit execution. + +Parameters: + +- `--execute` +- `--start-hour` +- `--end-hour` +- `--max-hours` +- `--sleep-ms` +- Optional source/owner diagnostics without changing mapping semantics. + +Process newest completed hours first so every successful step extends one contiguous interval backward from live capture: + +1. Deploy all live writers. +2. Record `live_capture_start_hour` as the first full UTC hour after every writer is active. +3. Rebuild the immediately preceding hour, then continue backward through the newest 7 days. +4. Continue the same contiguous sequence to 90 days before the public dashboard relies on 30d/90d evidence. +5. Retain the bootstrapped 90 days and all future rollups indefinitely. Older historical expansion is optional follow-up work, not a launch requirement. + +Before execution, run `EXPLAIN` against production-shaped data for every source query. Confirm `microdollar_usage.created_at` range scans, Exa partition pruning, and bounded credit-transaction/term scans. Do not add or replace indexes on the large raw usage table without a separate online-index rollout plan. + +For each completed pre-cutover hour: + +1. Aggregate all four canonical source families into temporary/staging results using half-open timestamp predicates. +2. Build owner/category totals and owner/category/source/driver buckets. +3. In one bounded `REPEATABLE READ` transaction, delete existing aggregate rows for that hour and insert absolute staged results. +4. Verify owner totals equal the sum of driver amounts and counts. +5. Commit. +6. Move `coverage_start_hour` back only when the new hour is contiguous with existing coverage. + +Absolute replacement makes reruns safe. Never reuse the live additive `total = total + excluded.total` behavior for backfill. Pre-cutover ranges run only after normal async persistence has drained; overlapping or unexpectedly late owner-hours use targeted advisory-locked repair instead of a global serializable transaction. + +Start with one hour per source scan and no concurrency. After benchmarks, permit a bounded multi-hour staging scan only if it reduces repeated raw-table IO while keeping replacement transactions and coverage advancement small. Bound statement and lock timeouts. Stop on elevated database load, replication lag, lock waits, or reconciliation differences. + +The deployment boundary and preceding hour need a second reconciliation pass after normal async usage persistence has drained. If a later source row exposes a gap, create a degraded interval first, repair affected owner-hours, reconcile the interval, then resolve it. + +### Targeted owner repair + +Add `repairOwnerSpendRollups(owner, startHour, endHourExclusive)` with an explicit maximum supplied by the caller. Future Spend Alert enablement uses a hard seven-day cap. Operator repair may use up to 90 days with lower concurrency and stricter timeouts. + +For each owner-hour: + +1. Acquire the same owner-hour advisory lock used by live capture. +2. Re-read every canonical source family for that owner and hour. +3. Build absolute totals and driver buckets. +4. Replace that owner's aggregate rows for the hour in one transaction. +5. Verify totals against drivers. + +Delete aggregate rows when the canonical result is zero. The repair path must be idempotent and safe to retry. + +### Reconciliation + +Add a dry-run reconciliation mode that compares rollups with canonical source sums for bounded owner/hour samples and reports: + +- Missing totals. +- Amount differences. +- Record-count differences. +- Driver sum differences. +- Unknown taxonomy values. +- Coverage holes. + +Run canaries for high-volume organizations, normal personal users, Exa users, KiloClaw users, and Coding Plan users before any alert evaluator consumes the tables. + +## Observability + +Instrument capture by source without logging sensitive request data: + +- Capture latency. +- Total and driver upsert failures. +- Advisory-lock wait duration. +- Transaction rollback count. +- Rows and microdollars captured by source/category. +- Backfill hour duration and staged row counts. +- Reconciliation mismatch count and amount. +- Coverage start and age. +- Unresolved degraded-interval count and age. +- Exact rolling-24h boundary-fragment query latency. + +Add Sentry context with source, category, owner type, and source record ID where available. Do not attach prompts, auth headers, cookies, tokens, Exa request bodies, user email, or display name. + +Monitor database tuple/advisory lock waits, WAL volume, index growth, autovacuum lag, replica lag, and AI Gateway/Exa persistence latency through existing database telemetry. + +## Tests + +### Schema and helper tests + +- Exactly-one-owner constraints. +- Controlled category/source constraints. +- UTC-hour normalization across session timezones and DST boundaries. +- Personal and organization uniqueness. +- Driver fallback normalization, length bounds, deterministic digest, and collision mismatch failure. +- Additive total and driver updates. +- Amount and record-count overflow/unsafe-integer rejection. +- Forced driver failure rolls back total and source spend transaction. +- Concurrent updates produce exact sums. +- Same owner/hour repair and live capture do not lose a contribution. + +### Source integration tests + +- AI personal positive spend updates raw usage, daily rollup, balance, total, and driver atomically. +- AI organization positive spend updates raw usage, organization balance, member daily usage, total, and driver atomically. +- AI zero-cost/BYOK rows create no Cost Insights rows. +- Charged Exa personal and organization requests produce Variable spend. +- Exa free allowance produces no Cost Insights spend. +- Coding Plan activation and renewal produce Scheduled spend once. +- KiloClaw enrollment and Worker renewal produce Scheduled spend once. +- Duplicate KiloClaw/Coding Plan paths do not increment rollups. +- KiloClaw settlement, credit grants, top-ups, expirations, refunds, and accounting adjustments produce no rollups. +- Rollup failure prevents the corresponding charge/source transaction from committing. + +### Read tests + +- 24h, 7d, 30d, and 90d queries return exact UTC bucket counts. +- Covered missing hours return zero. +- Uncovered and degraded hours remain marked unknown. +- Category totals equal bucket totals. +- Exact rolling-24h reads combine full rollup hours and raw boundary fragments without double counting. +- Top drivers aggregate combined dimensions and use deterministic tie-breaking. +- Personal and organization owner data cannot cross scopes. + +### Backfill and repair tests + +- Canonical source fixtures map to the same category/source/driver values as live capture. +- Backfill rerun produces identical totals and drivers. +- Failed hour does not move coverage. +- Empty source hour advances coverage without aggregate rows. +- Unresolved degraded intervals suppress zero-fill until repair and reconciliation resolve them. +- Targeted repair corrects missing and inflated rows. +- Targeted repair deletes rows whose canonical source sum is zero. +- Concurrent late source contribution plus repair is counted exactly once. +- Historical unknown fields use `other` rather than mutable current values. + +## Delivery sequence + +### Slice 0: spend-writer audit + +Repeat the direct balance-mutation audit, classify every production mutation, and fail planning/implementation review on unexplained writers. Record exclusions and canonical source identity in tests or repository-local code comments next to the central capture contract. + +Outcome: implementation has a closed producer inventory before it claims complete coverage. + +### Slice 1: schema and capture primitive + +Files: + +- `packages/db/src/schema-types.ts` +- `packages/db/src/schema.ts` +- `packages/db/src/cost-insights-rollups.ts` +- `packages/db/package.json` +- Generated migration and schema/helper tests + +Outcome: tables and transaction-bound capture helper exist, with no producer calling them yet. + +Generate migration with `pnpm drizzle generate`. Do not hand-write or edit generated migration SQL, snapshot, or journal. + +### Slice 2: already-transactional scheduled spend + +Integrate Coding Plan activation/renewal and KiloClaw web/Worker charge paths. These paths need the least transaction restructuring and validate the shared helper in both Next.js and Cloudflare Worker environments. + +Outcome: all Scheduled Credit spend dual-writes atomically. + +### Slice 3: AI Gateway transaction consolidation + +Refactor personal and organization AI persistence into one caller-owned transaction, add capture, and move organization low-balance email scheduling after commit. + +Outcome: AI Gateway Variable Credit spend dual-writes atomically for both owner types. + +### Slice 4: Exa transaction consolidation + +Combine Exa log, monthly counter, owner charge, organization member usage, and capture in one transaction. + +Outcome: all known Variable Credit spend paths dual-write atomically. + +### Slice 5: read repository and coverage + +Implement dense hourly evidence, current-hour totals, exact rolling-24h composition, top drivers, coverage, and degraded-interval reads. + +Outcome: application work can consume one Postgres datasource without knowing source ledgers. + +### Slice 6: backfill, repair, and shadow validation + +Implement canonical aggregation, dry-run reconciliation, 7-day then 90-day backfill, and targeted owner repair. + +Outcome: coverage reaches 90 days with zero reconciliation differences before alerts or dashboard data rely on it. + +## Verification commands + +Before database-backed tests, check Postgres with: + +```sh +docker compose -f dev/docker-compose.yml ps postgres +``` + +Start it if needed with `pnpm test:db`. + +Run the narrowest tests for each slice, including: + +```sh +pnpm --filter @kilocode/db typecheck +pnpm test -- apps/web/src/lib/ai-gateway/processUsage.test.ts +pnpm test -- apps/web/src/lib/exa-usage.test.ts +pnpm test -- apps/web/src/lib/coding-plans +pnpm --filter kiloclaw-billing test +scripts/typecheck-all.sh --changes-only +pnpm format +``` + +Use actual package/test paths discovered during implementation where directory arguments are unsupported. Full monorepo `pnpm typecheck` is not the default because repository guidance requires targeted checks unless the change breadth warrants the full run. + +Benchmark AI Gateway capture against production-shaped concurrency using the existing usage benchmark before rollout. Compare baseline versus rollup-enabled p50/p95/p99 persistence latency and inspect lock waits. + +## Acceptance criteria + +- Every production `microdollars_used` increment has an explicit included/excluded classification. +- AI Gateway, charged Exa, pure-credit KiloClaw, and Coding Plan spend update totals and one driver bucket atomically with the charge. +- Snowflake is absent from capture, reads used for correctness, repair, and backfill. +- A failed mandatory rollup write rolls back its source spend transaction. +- Duplicate billing attempts do not duplicate rollups. +- Hour keys are explicit UTC hours. +- Personal and organization totals cannot collide or leak across scopes. +- Covered zero-spend hours are distinguishable from unknown or degraded history. +- Exact rolling-24h reads use rollup interiors plus canonical raw boundary fragments. +- Newest 7 days reconcile before anomaly work starts; full 90 days reconcile before 30d/90d dashboard evidence is treated as complete. +- Bootstrapped and newly captured rollup rows have no retention expiry. +- Backfill and targeted repair are absolute, idempotent, bounded, and resumable. +- No email, display name, secret, prompt, or arbitrary request payload is persisted in rollup tables. +- Capture latency and lock contention stay within limits agreed from benchmark results. diff --git a/.plans/cost-insights.md b/.plans/cost-insights.md new file mode 100644 index 0000000000..74f2fece22 --- /dev/null +++ b/.plans/cost-insights.md @@ -0,0 +1,149 @@ +# Cost Insights Implementation Plan + +This plan covers Cost Insights v1, including Spend Alerts and Cost Suggestions. Durable product behavior and invariants live in `.specs/cost-insights.md`. `CONTEXT.md` owns canonical domain language. `.plans/cost-insights-data-layer.md` is the implementation plan for Credit-spend capture, owner-hour rollups, read primitives, coverage, backfill, and repair. + +## Status + +Draft plan. Core product decisions are confirmed. Storybook UI exists as design reference. Backend implementation has not started. + +## Confirmed Decisions + +- Cost Insights is account-level surface for Spend Alerts. +- Spend Alerts are opt-in for personal users and organizations. +- Spend Alerts are alert-only. +- Spend Alerts must not block spend, pause usage, throttle usage, suppress auto-top-up, reject paid requests, or emit Spend Alerts-specific HTTP 402 responses. +- Existing depleted-credit and low-balance billing behavior remains separate. +- Cost Insights v1 is publicly visible to eligible owners without a release-toggle gate. +- Cost Insights routes mirror Security Agent shape: dashboard plus settings for personal and organization owners. +- Cost Insights dashboard shows read-only recent spend evidence even when Spend Alerts are disabled. +- Cost Insights dashboard default evidence shows 24-hour spend summary plus 7-day hourly chart. +- Cost Insights dashboard supports preset evidence ranges: 24h, 7d, 30d, and 90d. +- Spend Alert settings expose only Spend Alert enablement and one optional spend threshold in v1. +- Cost Suggestions are enabled by default through an owner-scoped setting independent from Spend Alert enablement. +- Disabling Cost Suggestions suppresses new suggestion emails and active suggestion cards without removing prior suggestion activity from Cost Insight Event history. +- Active Cost Suggestions appear on the Cost Insights dashboard, with Spend Alerts taking visual and ordering priority when both are active. +- Cost Suggestions are advisory: they do not guarantee savings, purchase or change subscriptions, or alter spend behavior. +- V1 does not expose spend limits, spend pauses, anomaly sensitivity controls, custom anomaly multipliers, custom anomaly floors, custom recipients, product exclusions, model exclusions, or per-member Spend Alert policy. +- First enable immediately evaluates current anomaly state and configured spend threshold state. +- First enable can create alert email/banner when current spend already crosses enabled alert state. +- Disabling Spend Alerts keeps owner config row disabled rather than deleting it. +- Re-enabling Spend Alerts reuses existing saved settings unless changed. +- Re-enable immediately evaluates current rolling spend and current-hour anomaly state. +- While disabled, settings changes save only and do not evaluate controls, create events, or send emails. +- Spend Alerts evaluate all owner Credit spend, with anomaly alerts focused on hourly Variable Credit spend bursts. +- Organization Cost Insights is visible and manageable only by active organization owners and billing managers. +- Organization members without Cost Insights access are told to contact an organization owner or billing manager. +- Kilo admins may inspect Spend Alerts under existing admin patterns but cannot disable alerts or change customer Spend Alert settings in v1 without owner/billing-manager authority. +- Detection uses Postgres source-of-truth data, not Snowflake-only usage analytics. +- Spend Alerts use dedicated normalized tables for owner config, owner state, owner-hour totals, owner-hour driver buckets, rollup coverage, degraded coverage intervals, and Cost Insight Events. +- Owner-hour totals and driver buckets are sparse aggregates; covered zero-spend hours are derived at read time rather than stored as zero rows. +- Missing rows count as zero only inside reconciled coverage that does not overlap an unresolved degraded interval; uncovered or degraded hours remain unknown. +- Spend Alerts owner-hour totals and driver buckets are maintained for all owners, including owners who have not enabled alerts. +- Spend Alerts driver buckets use controlled taxonomy values, with `other` for unknown source classification. +- V1 source taxonomy is `ai_gateway`, `kiloclaw`, `coding_plan`, and `other`. +- V1 owner-hour totals and driver buckets are retained indefinitely. +- Owner-hour totals are keyed by spend category, with separate rows for Variable and Scheduled Credit spend. +- Owner-hour buckets use UTC hour start timestamps. +- Driver buckets are keyed by spend category as well as source and driver dimensions. +- Driver buckets and event snapshots may retain actor user IDs but must not copy actor email or actor display name. +- Driver buckets store actor user ID for both personal and organization spend; UI resolves labels from current user rows at render time. +- Driver buckets store total spend and contributing spend-record count. +- Every Credit spend path updates owner-hour totals and applicable driver buckets atomically with spend recording. +- Credit spend does not commit if corresponding owner-hour total or driver-bucket update fails. +- Enablement uses existing hourly owner rollups for baseline, with Postgres backfill or repair when rollups are missing. +- Enablement repair targets the prior 7 days; 30d and 90d dashboard evidence is not treated as complete until contiguous reconciled coverage reaches the requested range. +- Initial rollout bootstraps 90 days of Postgres evidence. Bootstrapped and newly captured rollups are retained indefinitely. +- Async evaluation uses current config at evaluation time. +- V1 anomaly detection uses product-managed fixed sensitivity. +- V1 anomaly threshold is `max(3 * baseline, 10 USD floor)` when baseline data is available. +- Owners without at least 24 completed hourly buckets use a 25 USD current-hour Variable Credit spend starter floor. +- Anomaly detection compares current partial-hour Variable Credit spend to full-hour threshold and can trigger before hour end. +- Anomaly baseline uses completed prior UTC-hour buckets and excludes current hour. +- Anomaly baseline includes zero-spend completed hours in trailing 7-day window. +- Owners with at least 24 completed hourly buckets use available-history p95 before 7 full days exist. +- Anomaly acknowledgment reviews current UTC-hour anomaly episode; future anomalous hours can alert again. +- Spend threshold is one optional USD cent value stored as microdollars. +- Spend threshold crossings create email, event history, and in-app review banner. +- Spend Threshold Alert evaluation uses exact elapsed `[asOf - 24h, asOf)` spend, not a 24-UTC-bucket approximation. +- Threshold review offers acknowledge, adjust threshold, or disable threshold; acknowledge alone is allowed. +- Threshold acknowledgment reviews current threshold-crossing episode until exact rolling spend falls below threshold and crosses again. +- Disabling threshold clears current threshold episode state. +- Config and review actions do not require reason text; events record actor, action, old/new values where applicable, and timestamp. +- Event history retains summarized Cost Insight Events for 90 days. +- Event history remains fixed to 90 days even though hourly rollups are retained indefinitely. +- Cost Insight Events are deleted after 90 days rather than merely hidden. +- Event retention is enforced by daily app cron deletion. +- Alert events snapshot top drivers at event creation time. +- Alert event snapshots include top 5 spend drivers. +- Events store direct evaluated settings in snapshots and do not require config version tracking in v1. +- Config events store changed fields plus resulting key settings, not full config snapshots. +- Owner state stores active episode dedupe/review state separately from 90-day event history. +- Owner state stores minimal current episode markers for anomaly hour, threshold crossing state, and review status. +- Spend Alerts store owner-scoped events separately from per-recipient notification delivery rows. +- Spend Alerts snapshot intended recipients at event creation and revalidate access before delivery. +- Notification delivery rows are deleted with parent event after 90 days. +- Active banners and review actions are visible to all current authorized managers, regardless of original email recipient snapshot. +- Every production `microdollars_used` mutation must be classified as included Credit spend or an explicit non-spend/accounting exclusion before rollup ingestion is considered complete. +- Data-layer implementation and rollout follow `.plans/cost-insights-data-layer.md`; physical schema, locking, driver-key, source-mapping, backfill, and repair mechanics remain there rather than being duplicated in this plan. +- Implementation should ship as independently verifiable vertical slices. + +## Vertical Slices + +Prerequisite: complete the spend-writer audit from `.plans/cost-insights-data-layer.md` so every production `microdollars_used` mutation has an included/excluded classification. + +| Slice | Goal | Primary outcomes | +|---|---|---| +| 1. Schema and policy primitives | Establish durable storage and pure policy contracts | Alert and suggestion config/state, owner-hour total, driver-bucket, coverage, degraded-interval, and event tables; shared policy helpers; defaults and validation | +| 2. Spend evidence data layer | Record and expose spend evidence across every Credit-spend path | Atomic Variable/Scheduled capture, dense hourly reads, exact rolling-24h reads, top drivers, 7-day enablement repair, 90-day bootstrap, reconciliation | +| 3. Spend Alert evaluation | Detect anomalies and threshold crossings | Async post-spend evaluation, hourly sweep, event creation, exact threshold semantics, episode dedupe | +| 4. Cost Suggestion evaluation | Create advisory cost-efficiency recommendations | Eligibility/evidence evaluation, active suggestion state, dismissal identity, suggestion events, CTA destinations | +| 5. Notifications and banners | Surface alerts and suggestions without request-side side effects | Email dispatch with per-recipient delivery rows, owner-scoped in-app banner, active suggestion cards, Cost Insights deep links | +| 6. Cost Insights UI | Let owners inspect evidence, configure features, and review outcomes | Dashboard, settings, event history, org member driver links, suggestion actions, sidebar attention state | +| 7. Retention and audit cleanup | Keep event history bounded while preserving rollups and dedupe state | Daily event deletion after 90 days, notification row cleanup, owner state remains compact, rollups remain indefinite | + +## Implementation Areas + +| Area | Expected change | +|---|---| +| `packages/db/src/schema.ts` | Add Cost Insights config, state, rollup, coverage, degraded-interval, suggestion, notification, and event tables. | +| `packages/db/src/cost-insights-rollups.ts` | Add transaction-bound capture primitive shared by web and Worker spend paths. | +| `packages/db/src/migrations/` | Generate migration from schema with `pnpm drizzle generate`. | +| `apps/web/src/lib/ai-gateway/` | Classify Variable Credit spend and atomically update owner-hour rollups without changing request admission. | +| `apps/web/src/lib/organizations/` | Consolidate organization spend mutation and defer existing low-balance email scheduling until commit. | +| `apps/web/src/lib/exa-usage.ts` | Capture charged Exa requests as Variable Credit spend under source `other` and product `exa`. | +| `apps/web/src/lib/kiloclaw/` | Classify pure-credit hosting enrollment as Scheduled Credit spend and update rollups. | +| `services/kiloclaw-billing/` | Capture pure-credit KiloClaw renewals inside existing Worker billing transactions. | +| `apps/web/src/lib/coding-plans/` | Classify plan purchases and renewals as Scheduled Credit spend when applicable. | +| `apps/web/src/lib/cost-insights/` | Add spend reads, coverage, backfill/repair, alert evaluation, Cost Suggestion evaluation, and event workflows. | +| `apps/web/src/routers/` | Add owner-scoped Cost Insights tRPC procedures. | +| `apps/web/src/app/(*)` | Add personal and organization Cost Insights routes. | +| `apps/web/src/components/` | Add dashboard, settings, banners, suggestions, and sidebar attention UI following existing app patterns. | +| `apps/web/src/emails/` | Add Spend Alert and Cost Suggestion emails with appropriate deep links. | + +## Required Tests + +- Alert and suggestion config defaulting, validation, independence, authorization, and organization billing-manager access. +- Closed spend-writer audit covering every production `microdollars_used` mutation. +- Hourly rollup writes for AI Gateway and charged Exa Variable Credit spend plus KiloClaw and Coding Plan Scheduled Credit spend. +- All-owner owner-hour total and driver-bucket writes plus enablement baseline reuse. +- Spend-write rollback when required owner-hour total or driver-bucket capture cannot commit. +- Covered zero-spend hours versus uncovered or degraded unknown hours. +- Prior-7-day enablement backfill/repair and reconciled 90-day completeness before 30d/90d evidence is treated as complete. +- Exact rolling `[asOf - 24h, asOf)` spend reads without UTC-bucket approximation or boundary double counting. +- Fixed anomaly formula (`max(3 * baseline, 10 USD floor)`), 25 USD starter floor, 7-day p95 baseline, and once-per-hour dedupe. +- Single spend-threshold crossing dedupe across exact rolling 24-hour windows. +- Cost Suggestion eligibility, default enablement, independent disablement, evidence windows, dismissal identity, CTA destination, and non-guarantee copy. +- Alert-only regression coverage for AI Gateway, Exa, KiloClaw, Coding Plan, and auto-top-up paths. +- Regression coverage that Spend Alerts and Cost Suggestions never reject paid requests or alter spend/subscription state. +- Org event evidence includes member spend drivers without exposing unauthorized org data. +- Sidebar attention state for unreviewed alert; suggestions alone do not use alert attention semantics. +- Email links route to the correct alert review or suggestion context. +- Per-recipient notification retry without duplicate owner-scoped events. +- Recipient access revalidation that skips org recipients who lost manager access before send. +- Current-manager banner, review, suggestion CTA, and dismissal visibility for owners and billing managers. + +## Verification + +- Run targeted tests for changed web, database, billing, usage, and Cost Insights areas. +- Run targeted type checking or `scripts/typecheck-all.sh --changes-only`; avoid full monorepo typecheck unless broad changes require it. +- Run `pnpm format` before commit. diff --git a/.specs/cost-insights.md b/.specs/cost-insights.md new file mode 100644 index 0000000000..d9da8e620c --- /dev/null +++ b/.specs/cost-insights.md @@ -0,0 +1,236 @@ +# Cost Insights + +## Role of This Document + +This spec defines business rules and invariants for Cost Insights, Spend Alerts, and Cost Suggestions. It is source of truth for owner scope, anomaly alerts, threshold alerts, alert review, cost-efficiency suggestions, event history, authorization, and user-facing behavior. It deliberately does not prescribe table names, handler names, queue plumbing, or UI component structure. + +## Status + +Draft -- created 2026-06-24. Updated 2026-06-24 to remove spend-blocking controls. Updated 2026-06-25 to rename the feature from Spend Insights to Cost Insights and add Cost Suggestions. + +## Conventions + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC 2119] [RFC 8174] when, and only when, they appear in all capitals, as shown here. + +## Definitions + +- **Cost Insights**: Dedicated account surface for viewing spend evidence and configuring Spend Alerts. +- **Spend Alerts**: Owner-scoped alerting capability for unusual or excessive Credit spend. +- **Spend owner**: Personal user or organization whose credit balance is charged for Credit spend. +- **Credit spend**: Existing Kilo billing concept for any operation that increments `microdollars_used`. +- **Variable Credit spend**: Credit spend created by request-metered product usage such as token usage or metered tool/API usage. +- **Scheduled Credit spend**: Predictable Credit spend created by subscription-like purchases, renewals, or hosting deductions. +- **Spend Anomaly Alert**: Spend Alert triggered when short-window owner Credit spend exceeds that owner's normal usage pattern. +- **Spend Threshold Alert**: Spend Alert triggered when rolling 24-hour owner Credit spend crosses the configured spend threshold. +- **Spend threshold**: Optional configured rolling 24-hour owner Credit-spend amount for Spend Threshold Alerts. +- **Alert acknowledgment**: Authorized owner action that marks the current alert episode as reviewed. +- **Cost Suggestion**: Owner-scoped recommendation based on observed Credit spend that offers an optional action to improve cost efficiency, such as moving eligible usage to a Coding Plan or Kilo Pass. +- **Suggestion dismissal**: Authorized owner action that hides a specific Cost Suggestion without changing spend, subscriptions, or future suggestion eligibility. +- **Cost Insight Event**: Durable owner-scoped record of Spend Alert notifications and reviews, Cost Suggestion creation and dismissal, configuration changes, and disablement. + +## Overview + +Cost Insights gives personal users and organizations visibility into Credit spend, unexpected increases, and optional ways to improve cost efficiency. Spend Alerts evaluates spend at the owner boundary, sends emails, and shows in-app review banners for anomaly and threshold events. Cost Suggestions uses observed spend to recommend an eligible Coding Plan or Kilo Pass when that option may reduce cost. + +Spend Alerts are alert-only. Cost Suggestions are advisory only. Neither capability MUST block spend, pause usage, throttle usage, suppress auto-top-up, reject paid requests, automatically purchase or change a subscription, or return Cost Insights-specific HTTP 402 responses. Existing low-balance and depleted-credit billing behavior remains separate from Cost Insights. + +Cost Insights does not replace low-balance alerts, auto-top-up setup, existing organization member daily limits, or product-specific subscription billing. It sits above product surfaces as owner-level spend insight, alerting, and cost-efficiency guidance. + +## Rules + +### Owner Scope + +1. Spend Alerts MUST belong to exactly one Spend owner: one personal user or one organization. +2. Spend Alerts MUST evaluate Credit spend at the Spend owner boundary, not per product by default. +3. All Credit spend charged to a Spend owner MUST count toward that owner's Spend Alert evaluation. +4. Spend Alerts MUST remain inactive until a Spend owner explicitly enables them. +5. Cost Insights v1 MUST be publicly visible to eligible owners without requiring a release-toggle gate. +6. First enabling Spend Alerts MUST immediately evaluate current anomaly and configured threshold state. +7. First enabling Spend Alerts MAY create alert email and banner when current spend already crosses enabled controls. +8. Disabling Spend Alerts MUST keep the owner config row disabled rather than deleting it. +9. Re-enabling Spend Alerts MUST reuse existing saved settings unless an authorized manager changes them. +10. Re-enabling Spend Alerts MUST immediately evaluate current rolling spend and current-hour anomaly state. +11. While Spend Alerts are disabled, settings changes MUST save only and MUST NOT evaluate controls, create Cost Insight Events, or send emails. + +### Authorization + +1. Personal Spend Alerts MUST be managed by the personal user. +2. Organization Spend Alerts MUST be visible and manageable only by active organization owners and billing managers. +3. Organization members who are not owners or billing managers MUST NOT view organization Cost Insights dashboard or settings. +4. Organization members without Cost Insights access SHOULD be told to contact an organization owner or billing manager. +5. Kilo admins MAY inspect Spend Alerts according to existing administrative access patterns. +6. Kilo admins MUST NOT disable Spend Alerts or change customer Spend Alert settings in v1 unless they also have owner or billing-manager authority for that owner. + +### Routes + +1. Personal Cost Insights dashboard MUST be served at `/cost-insights`. +2. Personal Cost Insights settings MUST be served at `/cost-insights/config`. +3. Organization Cost Insights dashboard MUST be served at `/organizations/[id]/cost-insights`. +4. Organization Cost Insights settings MUST be served at `/organizations/[id]/cost-insights/config`. +5. Cost Insights MUST appear in the Account section of personal and organization sidebars. +6. Cost Insights sidebar item MUST show attention state when owner has an unreviewed Spend Alert. +7. Cost Insights routes MUST NOT require a feature flag in v1. + +### Dashboard and Settings + +1. Cost Insights dashboard MUST show current alert state, review actions, and spend evidence. +2. Cost Insights settings MUST own Spend Alert enablement and spend threshold configuration. +3. V1 settings MUST expose Spend Alert enablement and one optional spend threshold. +4. V1 settings MUST NOT expose hard spend limits, spend pauses, throttles, product exclusions, model exclusions, custom recipients, anomaly sensitivity controls, custom anomaly multipliers, custom anomaly floors, or per-member Spend Alert policy. +5. Cost Insights dashboard MUST show read-only recent spend evidence even when Spend Alerts are disabled. +6. Cost Insights dashboard default evidence MUST show a 24-hour spend summary and 7-day hourly chart. +7. Cost Insights dashboard MUST support preset evidence ranges: 24h, 7d, 30d, and 90d. + +### Cost Suggestions + +1. Cost Suggestions MUST be enabled by default for every eligible Spend owner. +2. Cost Suggestions MUST have an owner-scoped setting independent from Spend Alert enablement. +3. Disabling Cost Suggestions MUST suppress new suggestion emails and active suggestion cards until the owner enables them again. +4. Disabling Cost Suggestions MUST NOT hide prior suggestion activity from Cost Insight Event history. +5. Cost Suggestions MUST be based on observed owner Credit spend and MUST identify the evidence window used for the recommendation. +6. V1 Cost Suggestions MAY recommend an eligible Coding Plan for concentrated model usage or Kilo Pass for pay-as-you-go usage. +7. A Cost Suggestion MUST state the recommended product or plan, the observed spend basis, and the additional credits, included usage, or other cost-efficiency benefit available under current plan terms without guaranteeing future savings. +8. A Cost Suggestion MUST provide one destination CTA that opens the relevant product, plan, pricing, or checkout location. +9. A Cost Suggestion MUST provide a dismissal action. +10. Suggestion dismissal MUST hide that specific active suggestion and create a Cost Insight Event. +11. Suggestion dismissal MUST NOT purchase a plan, modify billing, disable future Cost Suggestions, or acknowledge a Spend Alert. +12. Dismissed suggestions MUST NOT reappear unchanged for the same evaluation window. +13. A materially new evaluation MAY create a new Cost Suggestion after prior dismissal when observed spend, recommendation, price, plan, or eligibility changes. +14. Cost Suggestions MUST NOT be presented as alerts, warnings, required actions, or guaranteed savings. +15. Active Cost Suggestions MUST appear on the Cost Insights dashboard in addition to active Spend Alerts. +16. Spend Alerts MUST take visual and ordering priority over Cost Suggestions when both are active. +17. Cost Suggestion CTA and dismissal actions MUST be available to the same authorized users who can manage Cost Insights for the Spend owner. +18. Cost Suggestion evaluation and display MUST NOT depend on Spend Alerts being enabled. +19. Cost Suggestion emails MAY link directly to the relevant CTA destination or to Cost Insights suggestion context. + +### Anomaly Detection + +1. Spend Anomaly Alerts MUST detect hourly Credit-spend bursts, not only daily spend increases. +2. Spend Anomaly Alerts MUST evaluate bursty Variable Credit spend separately from predictable Scheduled Credit spend. +3. Spend Anomaly Alerts MUST use a Postgres owner-hourly spend rollup, not warehouse-only analytics. +4. Spend Alerts detection MUST NOT depend on Snowflake-only usage analytics. +5. Default Spend Anomaly Alert baseline MUST be trailing 7-day hourly p95 Variable Credit spend. +6. Spend Anomaly Alert baseline MUST use completed prior UTC-hour buckets and exclude the current UTC hour. +7. Spend Anomaly Alert baseline MUST include zero-spend completed hours in the trailing 7-day window. +8. Owners with at least 24 completed hourly buckets MUST use available-history p95 even before 7 full days exist. +9. Spend Anomaly Alerts MUST evaluate current partial-hour Variable Credit spend against the full-hour anomaly threshold. +10. Spend Anomaly Alerts MAY trigger before the current UTC hour ends. +11. Owners without at least 24 hourly baseline buckets MUST use a starter current-hour Variable Credit spend floor. +12. V1 Spend Anomaly Alert sensitivity MUST be product-managed and fixed. +13. V1 Spend Anomaly Alert threshold MUST be calculated as `max(3 * baseline, 10 USD floor)` when baseline data is available. +14. V1 starter anomaly floor MUST be 25 USD of current-hour Variable Credit spend. +15. Spend Anomaly Alerts MAY fire at most once per owner per hour while anomalous spend persists. +16. Alert acknowledgment MUST review the current UTC-hour anomaly episode. +17. Future anomalous UTC hours MAY create new Spend Anomaly Alerts after prior hour acknowledgment. +18. Spend Anomaly Alerts MUST use separate notification identity from Spend Threshold Alerts. + +### Spend Threshold Alerts + +1. Spend threshold MUST be a single optional USD amount in v1. +2. Spend threshold values MUST be stored as microdollars. +3. Spend threshold UI MUST accept and display USD amounts. +4. Spend threshold input MUST accept positive USD amounts with cent precision only. +5. Spend threshold input MUST reject amounts with more than two decimal places. +6. Spend threshold input MUST NOT enforce a product-level maximum. +7. Spend Threshold Alerts MUST evaluate all rolling 24-hour owner Credit spend, including Variable Credit spend and Scheduled Credit spend. +8. Spend Threshold Alerts MUST fire once per below-to-above threshold crossing. +9. Spend Threshold Alerts MAY fire again only after rolling spend drops below the threshold and later crosses it again. +10. Spend Threshold Alerts MUST create email, Cost Insight Event history, and in-app review banner. +11. Threshold review MUST offer acknowledge, adjust threshold, or disable threshold. +12. Threshold review MUST allow acknowledge without requiring threshold changes. +13. Threshold acknowledgment MUST review the current threshold-crossing episode until rolling spend falls below threshold and crosses again. +14. Disabling the spend threshold MUST clear current threshold episode state. + +### Rollups and Evidence + +1. Spend Alerts MUST use dedicated normalized storage for owner configuration, owner state, owner-hour totals, owner-hour driver buckets, and Cost Insight Events. +2. Owner-hour totals MUST record all Credit spend for all Spend owners, including owners who have not enabled Spend Alerts. +3. Owner-hour total entries MUST label spend category so anomaly evaluation can distinguish Variable Credit spend from Scheduled Credit spend. +4. Owner-hour totals MUST be keyed by spend category with separate rows for Variable Credit spend and Scheduled Credit spend. +5. Owner-hour buckets MUST use UTC hour start timestamps. +6. Spend Alerts MUST store compact owner-hour driver buckets separately from owner-hour totals. +7. Driver buckets MUST be keyed by spend category as well as source and driver dimensions. +8. Driver buckets SHOULD group spend by product or feature, model or provider, and actor user where applicable. +9. Driver buckets MUST use controlled taxonomy values for Spend Alerts-owned dimensions. +10. Unknown source classification MUST map to `other`, not arbitrary source-specific labels. +11. V1 source taxonomy MUST include `ai_gateway`, `kiloclaw`, `coding_plan`, and `other`. +12. Driver buckets MUST store actor user ID for both personal and organization spend. +13. Driver buckets MUST store total spend and contributing spend-record count. +14. Every Credit spend path MUST update owner-hour totals and applicable driver buckets atomically with spend recording. +15. Credit spend MUST NOT commit unless the corresponding owner-hour total and applicable driver-bucket updates also commit. +16. Spend Alert evaluation and notification side effects SHOULD run asynchronously after spend recording. +17. Enabling Spend Alerts MUST use already-maintained owner-hour totals for baseline data when available. +18. Enabling Spend Alerts MUST backfill or repair the owner's last 7 days of hourly baseline from Postgres historical usage data when rollups are missing or incomplete. +19. Baseline backfill and repair MUST use Postgres source-of-truth data, not Snowflake. + +### Notifications + +1. Spend Alerts v1 MUST send email and show owner-scoped in-app banner until alert acknowledgment. +2. Spend Alerts v1 MUST NOT send mobile or push notifications. +3. Personal Spend Alerts MUST be sent to the personal user's email. +4. Organization Spend Alerts MUST be sent to active organization owners and billing managers. +5. Spend Alert emails MUST link to Cost Insights dashboard review context. +6. Spend Alerts MUST store owner-scoped Cost Insight Events separately from per-recipient notification delivery rows. +7. Per-recipient notification delivery rows MAY be retried without creating duplicate owner-scoped Cost Insight Events. +8. Spend Alerts MUST snapshot intended notification recipients at event creation. +9. Spend Alerts MUST revalidate recipient access before email delivery. +10. Organization recipients who no longer have authorized access at dispatch time MUST NOT receive Spend Alert email. +11. Managers added after event creation SHOULD NOT receive already-created alert emails unless a new alert event is created. +12. Active Spend Alert in-app banners MUST be visible to all current authorized managers. +13. Alert review actions MUST be available to all current authorized managers. +14. Banner visibility MUST NOT be limited to users snapshotted as notification recipients. +15. Spend Alert notification delivery rows MUST be deleted with their parent Cost Insight Event after 90 days. + +### Event History + +1. Cost Insights MUST retain and display 90 days of Cost Insight Events. +2. Cost Insight Event history MUST remain fixed to 90 days even though hourly rollups are retained indefinitely. +3. Cost Insight Events MUST be deleted after 90 days rather than merely hidden. +4. Cost Insight Event retention MUST be enforced by daily app cron deletion. +5. Cost Insight Events MUST include configuration changes, anomaly alerts, threshold alerts, reviews, Cost Suggestion creation, Cost Suggestion dismissal, and disablement. +6. Cost Insight Events MUST store summarized decision snapshots. +7. Cost Insight Events MUST NOT copy raw request rows. +8. Summary snapshots SHOULD include threshold, rolling spend totals, current-hour variable spend, baseline, and top driver dimensions. +9. Top driver dimensions SHOULD include product or feature, model or provider, and organization member when applicable, with spend and request counts. +10. Alert Cost Insight Events MUST snapshot top drivers at event creation time. +11. Alert Cost Insight Events MUST snapshot top 5 spend drivers. +12. Cost Insight Events MUST store direct evaluated settings in snapshots. +13. Cost Insight Events MUST NOT require config version tracking in v1. +14. Spend Alert config events MUST store changed fields plus resulting key settings. +15. Spend Alert config events MUST NOT store full config snapshots in v1. +16. Cost Insight Events MUST NOT copy actor email or actor display name into snapshots. +17. Cost Insight Events MAY retain actor user IDs because soft-deleted user rows are anonymized. +18. Cost Insights UI MUST resolve actor display labels from current user rows at render time. +19. Spend Alerts MUST NOT depend on event-time actor display labels for org member driver display. +20. Owner state MUST store active episode dedupe and review state separately from 90-day event history. +21. Deleting expired Cost Insight Events MUST NOT cause old threshold or anomaly episodes to alert again unless the episode legitimately recrosses or reoccurs. +22. Owner state SHOULD store minimal current episode markers for anomaly hour, threshold crossing state, and review status. +23. Owner state MUST NOT duplicate full Cost Insight Event snapshots. + +### Rollup Retention + +1. V1 owner-hour totals MUST be retained indefinitely. +2. V1 owner-hour driver buckets MUST be retained indefinitely. +3. Indefinite rollup retention MUST NOT change the 90-day display window for Cost Insight Events. +4. Owner-hour driver buckets MAY retain actor user IDs indefinitely because soft-deleted user rows are anonymized. +5. Owner-hour driver buckets MUST NOT store actor email or actor display name. + +### Organization Member Actions + +1. Organization Cost Insights dashboard SHOULD identify organization member spend drivers when applicable. +2. Organization Cost Insights dashboard SHOULD link to existing organization member daily limit controls. +3. V1 MUST NOT add separate per-member Spend Alert policy. + +### Evaluation and Non-Enforcement + +1. Spend Alert evaluation MUST run asynchronously after Credit spend updates. +2. Spend Alerts MUST also run an hourly sweep to catch missed evaluations and rolling-window transitions. +3. Async Spend Alert evaluation MUST use current config at evaluation time. +4. V1 Spend Alert evaluation SHOULD run in `apps/web` through post-spend async execution and app cron hourly sweep. +5. Spend Alerts MUST NOT prevent any Credit spend path from running because of anomaly state, threshold state, alert review state, or owner Spend Alert configuration. +6. Spend Alerts MUST NOT alter auto-top-up eligibility or execution. +7. Spend Alerts MUST NOT use the existing `usage_limit_exceeded` error type for alert or threshold state. + +## Decision References + +- `CONTEXT.md` defines canonical Cost Insights and Spend Alerts language. diff --git a/AGENTS.md b/AGENTS.md index 191eb43b3c..84b4478684 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,6 +134,7 @@ Business-rule specs live in `.specs/`. Before making **any** changes to a domain | `.specs/kiloclaw-controller.md` | KiloClaw controller/machine lifecycle, bootstrap, Docker image | | `.specs/kiloclaw-datamodel.md` | KiloClaw data model — instance/subscription tables, invariants | | `.specs/model-experiments.md` | Model experiment routing, bucketing, lifecycle, prompt retention, and reporting rules | +| `.specs/cost-insights.md` | Cost Insights and Spend Alerts owner scope, anomaly alerts, threshold alerts, and alert acknowledgments | | `.specs/security-agent.md` | Security Agent Auto Remediation and finding/SLA notification guarantees | | `.specs/subscription-center.md` | Subscription Center ownership, states, and user-facing behavior | | `.specs/team-enterprise-seat-billing.md` | Team and Enterprise seat billing, subscription management | diff --git a/CONTEXT.md b/CONTEXT.md index 1b92c7a7bd..3e9adfe412 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,7 +2,7 @@ ## Scope -Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contract defines Code Reviewer and Security Agent language plus ownership boundaries used across review execution, analytics, sync, web, email, remediation, tests, and product documentation. +Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contract defines Code Reviewer, Security Agent, and Cost Insights language plus ownership boundaries used across review execution, analytics, sync, web, email, remediation, billing alerts, tests, and product documentation. ## Contexts @@ -13,6 +13,7 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr | **Security Sync** | Dependabot synchronization, finding persistence, notification eligibility, recipient intent materialization, and durable notification state | `services/security-sync/` | Event state remains owner-scoped; email sending does not occur inside finding persistence transactions | | **Security Agent Email Delivery** | Dispatch-time revalidation, email rendering, owner-aware links, and Mailgun delivery | `apps/web/src/app/api/internal/security-agent/`, `apps/web/src/lib/email.ts`, `apps/web/src/emails/` | Accepts notification identity only and loads current data before sending | | **Shared Security Notification Policy** | Canonical config parsing, defaults, severity thresholds, and pure event eligibility rules | `packages/worker-utils/src/security-notification-policy.ts` | Web and Worker must use same policy contract | +| **Cost Insights** | Spend evidence dashboard, Spend Alerts policy, alert history, and owner-scoped spend alerting | Billing, usage ingestion, usage analytics, and subscription-management surfaces | Applies to both personal users and organizations | ## Canonical Terms @@ -36,6 +37,18 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr | **Email Delivery** | Attempt to render and send one Security Agent Notification through Mailgun | Referring to provider side effect, retry, or acceptance | Notification event | | **Security Finding Activity Event** | Immutable record of one material user, system-policy, or source-driven action or outcome that changes or explains a Security Finding | Referring to evidence included in a Security Agent Audit Report | Page view, unchanged sync observation, queue claim, heartbeat | | **Security Agent Audit Report** | Owner-scoped, period-bounded audit view of Security Finding Activity Events grouped by Security Finding | Referring to the interactive audit report | Generic audit-log export, activity dump | +| **Cost Insights** | Dedicated Account-section surface for viewing spend evidence, configuring Spend Alerts, and acting on Cost Suggestions | Naming the product surface, dashboard, settings, routes, or sidebar item | Spend Protection, Cost Controls | +| **Spend Alerts** | Owner-scoped alerting capability for unusual or excessive Credit spend | Referring to alert evaluation, emails, banners, settings, or notification policy | Spend Protection, hard limit, spend blocker | +| **Cost Suggestion** | Optional owner-scoped recommendation based on observed Credit spend that may improve cost efficiency through an eligible Coding Plan or Kilo Pass | Referring to recommendation evaluation, dashboard cards, emails, CTA destinations, dismissal, or settings | Alert, warning, guaranteed savings, automatic optimization | +| **Suggestion dismissal** | Authorized owner action that hides one Cost Suggestion without changing billing or future suggestion eligibility | Referring to dismissing a recommendation | Alert acknowledgment, unsubscribe, disable suggestions | +| **Spend owner** | Personal user or organization whose credit balance is charged for Credit spend | Referring to the Spend Alerts policy and evaluation boundary | Account when personal/org ambiguity matters | +| **Spend Anomaly Alert** | Spend Alert triggered when short-window owner Credit spend exceeds that owner's normal usage pattern | Referring to hourly burst-detection Spend Alerts | Low-balance alert, threshold alert | +| **Variable Credit spend** | Credit spend created by request-metered product usage such as token usage or metered tool/API usage | Referring to spend that can burst unexpectedly during active usage | Scheduled Credit spend | +| **Scheduled Credit spend** | Predictable Credit spend created by subscription-like purchases, renewals, or hosting deductions | Referring to expected recurring or explicitly purchased credit deductions | Variable Credit spend | +| **Spend Threshold Alert** | Spend Alert triggered when rolling 24-hour owner Credit spend crosses configured spend threshold | Referring to threshold notification identity or review | Warning threshold, critical threshold, quota | +| **Spend threshold** | Optional configured rolling 24-hour owner Credit-spend amount for Spend Threshold Alerts | Referring to the single v1 threshold setting | Hard limit, budget cap, daily quota | +| **Alert acknowledgment** | Authorized owner action that marks the current alert episode as reviewed | Referring to review without changing settings | Email open, page view, passive acknowledge | +| **Cost Insight Event** | Durable owner-scoped record of Spend Alert notifications, reviews, configuration changes, and disablement | Referring to 90-day Cost Insights history | Raw usage row, provider log | ## Relationships @@ -51,6 +64,89 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr - An **Email Delivery** realizes a durable **Security Agent Notification** and may be retried without creating new event identity. - A **Security Remediation** belongs to one **Security Finding** and can have one or more **Security Remediation Attempts**. - A **Security Finding Activity Event** belongs to one Security Agent owner and one Security Finding, including after that finding is deleted. +- **Spend Alerts** and **Cost Suggestions** belong to exactly one **Spend owner**: one personal user or one organization. +- All Credit spend charged to a **Spend owner** counts toward that owner's Spend Alerts and Cost Suggestion evaluation. +- **Cost Suggestions** are enabled by default, independent from Spend Alerts, and recommend an eligible Coding Plan or Kilo Pass when observed Credit spend indicates potential cost-efficiency benefit. +- Cost Suggestions are advisory. They do not guarantee savings, automatically purchase or change subscriptions, or alter spend behavior. +- Every active Cost Suggestion provides one destination CTA and a **Suggestion dismissal** action. +- Suggestion dismissal hides that specific recommendation, creates a **Cost Insight Event**, and does not disable future materially different Cost Suggestions. +- Disabling Cost Suggestions suppresses new suggestion emails and active dashboard suggestions but preserves prior suggestion activity history. +- Cost Insights v1 is publicly visible to eligible owners without a release-toggle gate. +- **Spend Alerts** are inactive until a **Spend owner** explicitly enables them. +- First enable immediately evaluates current anomaly state and configured **Spend threshold** state. +- First enable can create alert email and banner when current spend already crosses enabled alert state. +- **Spend Alerts** are alert-only. They do not block spend, pause usage, throttle usage, suppress auto-top-up, reject paid requests, or return Spend Alerts-specific HTTP 402 responses. +- Existing low-balance and depleted-credit billing behavior remains separate from **Spend Alerts**. +- **Spend Anomaly Alerts** detect hourly Credit-spend bursts, not only daily spend increases. +- **Spend Anomaly Alerts** evaluate bursty **Variable Credit spend** separately from predictable **Scheduled Credit spend**. +- **Spend Anomaly Alerts** use a Postgres owner-hourly spend rollup, not warehouse-only hourly analytics. +- Spend Alerts hourly rollups are maintained for all **Spend owners**, including owners who have not enabled Spend Alerts. +- V1 **Spend Anomaly Alert** sensitivity is product-managed and fixed; users cannot configure sensitivity, custom multipliers, or custom floors. +- Default **Spend Anomaly Alert** baseline is trailing 7-day hourly p95 **Variable Credit spend**. +- Spend Anomaly Alert baseline uses completed prior UTC-hour buckets and excludes the current UTC hour. +- Owners with at least 24 completed hourly buckets use available-history p95 even before 7 full days exist. +- Owners without at least 24 hourly baseline buckets use a starter current-hour **Variable Credit spend** floor for **Spend Anomaly Alerts**. +- **Spend Anomaly Alerts** may fire at most once per owner per hour while anomalous spend persists. +- **Spend Threshold Alerts** use separate notification identity from **Spend Anomaly Alerts**. +- **Spend threshold** is a single optional rolling 24-hour USD amount stored as microdollars and displayed with cent precision. +- **Spend Threshold Alerts** evaluate all rolling 24-hour owner Credit spend, including **Variable Credit spend** and **Scheduled Credit spend**. +- **Spend Threshold Alerts** fire once per below-to-above threshold crossing and may fire again only after rolling spend drops below the threshold and later crosses it again. +- **Alert acknowledgment** reviews the current anomaly or threshold episode without requiring settings changes. +- Threshold review offers acknowledge, adjust threshold, or disable threshold; acknowledge alone is allowed. +- **Spend Alerts** are sent to the personal user's email for personal owners and to active organization owners and billing managers for organization owners. +- Spend Alerts store owner-scoped **Cost Insight Events** separately from per-recipient notification delivery rows. +- Spend Alerts snapshot intended notification recipients at event creation and revalidate recipient access before delivery. +- Spend Alerts notification delivery rows are deleted with their parent **Cost Insight Event** after 90 days. +- **Spend Alerts** v1 sends email and shows an owner-scoped in-app banner until **Alert acknowledgment**. It does not send mobile or push notifications in v1. +- Active Spend Alert banners and review actions are visible to all current authorized managers, regardless of original email recipient snapshot. +- Spend Alert emails deep-link to the Cost Insights dashboard review context, not settings-first flow. +- Cost Insights retains and displays 90 days of **Cost Insight Events**. +- **Cost Insight Events** include configuration changes, anomaly alerts, threshold alerts, reviews, and disablement. +- Cost Insight Event history remains fixed to 90 days even though hourly rollups are retained indefinitely. +- Cost Insight Events are deleted after 90 days rather than merely hidden. +- Cost Insight Event retention is enforced by daily app cron deletion. +- **Cost Insight Events** store summarized decision snapshots such as threshold, rolling spend totals, current-hour variable spend, baseline, and top driver dimensions. They do not copy raw request rows. +- Alert **Cost Insight Events** snapshot top 5 spend drivers at event creation time. +- Cost Insight Events store direct evaluated settings in snapshots and do not require config version tracking in v1. +- Spend Alert config events store changed fields plus resulting key settings, not full config snapshots. +- **Cost Insights** is the dedicated Account-section surface for Spend Alerts: `/cost-insights` and `/organizations/[id]/cost-insights` are dashboard routes; `/cost-insights/config` and `/organizations/[id]/cost-insights/config` are settings routes. +- Cost Insights dashboard shows current alert state, review actions, and spend evidence. Cost Insights settings owns Spend Alerts policy. +- Account sidebar Cost Insights item shows attention state for unreviewed Spend Alert. +- Organization Cost Insights identifies member spend drivers and links to existing organization member daily limit controls; v1 does not add per-member Spend Alert policy. +- Organization Cost Insights dashboard and settings are visible only to organization owners and billing managers. +- Organization members who cannot view Cost Insights are told to contact an organization owner or billing manager. +- Kilo admins may inspect Spend Alerts under existing admin patterns, but v1 disable and settings changes require owner or billing-manager authority. +- Spend Alert config and review actions do not require reason text in v1; events record actor, action, old and new values where applicable, and timestamp. +- Disabling Spend Alerts keeps the owner config row disabled rather than deleting it. +- Re-enabling Spend Alerts reuses existing saved settings unless an authorized manager changes them. +- Re-enabling Spend Alerts immediately evaluates current rolling spend and current-hour anomaly state. +- While Spend Alerts are disabled, settings changes save only and do not evaluate controls, create events, or send emails. +- Cost Insights dashboard shows read-only recent spend evidence even when Spend Alerts are disabled. +- Cost Insights dashboard default evidence shows a 24-hour spend summary plus a 7-day hourly chart. +- Cost Insights dashboard supports preset evidence ranges: 24h, 7d, 30d, and 90d. +- Spend Alerts owner state stores active episode dedupe and review state separately from 90-day event history. +- Spend Alerts owner state stores minimal current episode markers for anomaly hour, threshold crossing state, and review status. +- Spend Alerts use dedicated normalized storage for owner configuration, owner state, hourly spend rollups, and **Cost Insight Events**. +- V1 Cost Insights settings expose Spend Alert enablement and one optional **Spend threshold** only. +- Enabling Spend Alerts uses already-maintained owner hourly rollups for baseline data, with Postgres source-of-truth backfill or repair when rollups are missing. +- Spend Alerts store owner-hour totals separately from compact owner-hour driver buckets. +- Spend Alerts owner-hour totals record all Credit spend and label spend category so anomaly evaluation can distinguish **Variable Credit spend** from **Scheduled Credit spend**. +- Spend Alerts owner-hour totals are keyed by spend category, with separate rows for Variable Credit spend and Scheduled Credit spend. +- Spend Alerts owner-hour buckets use UTC hour start timestamps. +- Spend Alerts driver buckets group owner-hour spend by compact dimensions such as product or feature, model or provider, and actor user where applicable. +- Spend Alerts driver buckets are keyed by spend category as well as source and driver dimensions. +- Spend Alerts driver buckets use controlled taxonomy values, with `other` for unknown source classification. +- V1 Spend Alerts source taxonomy is `ai_gateway`, `kiloclaw`, `coding_plan`, and `other`. +- Spend Alerts owner-hour totals and driver buckets are retained indefinitely in v1. +- Spend Alerts driver buckets may retain actor user IDs indefinitely because soft-deleted user rows are anonymized. Driver buckets and event snapshots must not copy actor email or actor display name. +- Spend Alerts store actor user IDs in driver buckets for both personal and organization spend; UI resolves member labels from current user rows at render time. +- Spend Alerts driver buckets store total spend and contributing spend-record count. +- Every Credit spend path updates the Spend Alerts hourly rollup atomically with spend recording. +- Credit spend must not commit unless the corresponding Spend Alerts hourly rollup update also commits. +- Spend Alerts evaluation runs asynchronously after Credit spend updates and through an hourly sweep that catches missed evaluations and rolling-window transitions. +- Async Spend Alerts evaluation uses current config at evaluation time. +- V1 Spend Alerts evaluation runs in `apps/web` through post-spend async execution and an app cron hourly sweep. +- Organization **Spend Alerts** are managed by organization owners and billing managers. - A **Security Finding Activity Event** falls into a report period based on when Kilo recorded or applied it. External source timestamps are supporting evidence and do not determine report inclusion. - A **Security Agent Audit Report** groups every matching reportable **Security Finding Activity Event** recorded by Kilo in the selected period. - V1 reports persisted SLA evidence only when it can do so from trustworthy recorded data. It does not calculate historical SLA compliance percentages or introduce new SLA lifecycle semantics. @@ -83,6 +179,10 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr - Do not call organization members or billing managers **Notification Recipients** unless they also hold current organization `owner` role. - Treat "all activity" in a **Security Agent Audit Report** as all material actions and outcomes recorded by Kilo, not every internal processing step or an attestation that legacy history is exhaustive. Exclude reads, unchanged sync observations, queue claims, heartbeats, and retries with no new finding-level outcome. - A rollout baseline event records current state at actual capture time for an existing Security Finding; it is not a synthetic creation event and must not be backdated. +- Use **Cost Insights** for the user-facing surface, **Spend Alerts** for the alerting capability, and **Cost Suggestion** for optional cost-efficiency recommendations. Do not call this feature Spend Protection or Cost Controls. +- Do not describe a Cost Suggestion as an alert, warning, guaranteed savings, automatic optimization, or required action. +- Use **Spend Threshold Alert** and **Spend threshold** for the single v1 threshold model. Do not introduce warning or critical threshold tiers unless the spec changes. +- Keep Spend Alerts alert-only. Do not describe them as spend blocks, hard limits, pauses, throttles, or request admission controls. ## Ambiguities @@ -103,11 +203,22 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr - **Security Agent** owns product policy, settings, permissions, and user-visible finding/remediation outcomes. - **Security Sync** owns finding synchronization, notification event admission, recipient intent materialization, deduplication, and durable state transitions. - **Security Agent Email Delivery** may revalidate and deliver an existing notification but must not create notification eligibility or copy mutable finding data into Worker request. +- Spend Alerts email delivery may retry per-recipient delivery rows but must not create duplicate owner-scoped Cost Insight Events. +- Spend Alerts email delivery must not send to an organization recipient who no longer has authorized access at dispatch time. +- Treat active Spend Alerts banner visibility as current owner state, not notification-recipient history. - **Shared Security Notification Policy** defines common parsing and pure eligibility behavior; it does not perform persistence or recipient lookup. - Cross-context dispatch sends only stable notification ID from **Security Sync** to authenticated **Security Agent Email Delivery** boundary. +- **Spend Alerts** evaluate personal and organization Credit spend at the owner boundary, not per product by default. +- Do not assume Spend Alerts apply to owners who have not opted in. +- Do not make Spend Alerts block, pause, throttle, suppress auto-top-up, reject paid requests, or emit Spend Alerts-specific HTTP 402 responses. +- Do not hide v1 Cost Insights behind a release-toggle gate unless a later product decision supersedes public opt-in. +- Do not make Spend Alerts depend on Snowflake-only usage analytics for detection. +- Treat organization owners and billing managers as authorized managers for organization Spend Alerts. +- Surface Spend Alerts through **Cost Insights** dashboard and settings routes, not as only an embedded usage, credits, or subscriptions control. ## Decision References +- `.specs/cost-insights.md` defines Cost Insights and Spend Alerts business rules. - `.plans/code-review-analytics.md` defines prospective Review Analytics collection, taxonomy, persistence, and metric semantics. - `.specs/security-agent.md` defines Security Agent Auto Remediation and notification guarantees. - `.plans/security-agent-notifications.md` records notification implementation and rollout design. diff --git a/apps/storybook/stories/Sidebar.stories.tsx b/apps/storybook/stories/Sidebar.stories.tsx index cd8d2c5a91..fb497af673 100644 --- a/apps/storybook/stories/Sidebar.stories.tsx +++ b/apps/storybook/stories/Sidebar.stories.tsx @@ -6,6 +6,7 @@ import { Building2, Cable, ChartColumnIncreasing, + ChartLine, ChevronLeft, ChevronRight, Cloud, @@ -108,6 +109,11 @@ const dashboardItems: SidebarStoryItem[] = [ icon: ChartColumnIncreasing, url: '/usage', }, + { + title: 'Cost Insights', + icon: ChartLine, + url: '/cost-insights', + }, ]; const kiloClawItems: SidebarStoryItem[] = [ diff --git a/apps/storybook/stories/cost-insights/AskKilo.stories.tsx b/apps/storybook/stories/cost-insights/AskKilo.stories.tsx new file mode 100644 index 0000000000..d2b5516a2e --- /dev/null +++ b/apps/storybook/stories/cost-insights/AskKilo.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/nextjs'; +import { CostInsightsAskKiloView, CostInsightsShellView } from '@/components/cost-insights'; +import { personalOwner } from './costInsightsFixtures'; + +const meta: Meta = { + title: 'Cost Insights/Ask Kilo', + component: CostInsightsAskKiloView, + parameters: { layout: 'fullscreen' }, +}; + +export default meta; +type Story = StoryObj; + +function AskKiloStory() { + return ( + + + + ); +} + +export const Conversation: Story = { + render: () => , +}; diff --git a/apps/storybook/stories/cost-insights/CostInsightsAlertBar.stories.tsx b/apps/storybook/stories/cost-insights/CostInsightsAlertBar.stories.tsx new file mode 100644 index 0000000000..b014908f2f --- /dev/null +++ b/apps/storybook/stories/cost-insights/CostInsightsAlertBar.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/nextjs'; +import { CostInsightsAlertBar } from '@/components/cost-insights'; +import { organizationOwner } from './costInsightsFixtures'; + +const meta = { + title: 'Cost Insights/In-App Alert Bar', + component: CostInsightsAlertBar, + parameters: { layout: 'fullscreen' }, + args: { + owner: organizationOwner, + alertCount: 2, + }, + decorators: [ + Story => ( +
+
+ Kilo Cloud +
+ +
+
Account overview
+
+
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const AlertsNeedReview: Story = {}; diff --git a/apps/storybook/stories/cost-insights/EventHistory.stories.tsx b/apps/storybook/stories/cost-insights/EventHistory.stories.tsx new file mode 100644 index 0000000000..071e05c66a --- /dev/null +++ b/apps/storybook/stories/cost-insights/EventHistory.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from '@storybook/nextjs'; +import { + CostInsightsEventHistoryView, + CostInsightsShellView, + type CostInsightsOwner, + type CostInsightEvent, +} from '@/components/cost-insights'; +import { + allEvents, + longLabelEvents, + organizationOwner, + personalOwner, +} from './costInsightsFixtures'; + +const paginatedEvents = Array.from({ length: 23 }, (_, index): CostInsightEvent => { + const event = allEvents[index % allEvents.length]; + if (!event) throw new Error('Activity fixture requires at least one event'); + return { + ...event, + id: `${event.id}-${index}`, + timestampLabel: + index < 5 ? event.timestampLabel : `${Math.floor(index / 5) + 1} days ago, 09:15`, + }; +}); + +const meta: Meta = { + title: 'Cost Insights/Activity', + component: CostInsightsEventHistoryView, + parameters: { layout: 'fullscreen' }, +}; + +export default meta; +type Story = StoryObj; + +function renderActivity( + events: CostInsightEvent[], + owner: CostInsightsOwner = personalOwner, + empty = false +) { + return ( + + + + ); +} + +export const ActivityHistory: Story = { + render: () => renderActivity(paginatedEvents, organizationOwner), +}; + +export const Empty: Story = { + render: () => renderActivity([], personalOwner, true), +}; + +export const Loading: Story = { + render: () => ( + + + + ), +}; + +export const LoadError: Story = { + render: () => ( + + + + ), +}; + +export const Mobile: Story = { + render: () => renderActivity(longLabelEvents, organizationOwner), + globals: { + viewport: { value: 'mobile2', isRotated: false }, + }, +}; diff --git a/apps/storybook/stories/cost-insights/Overview.stories.tsx b/apps/storybook/stories/cost-insights/Overview.stories.tsx new file mode 100644 index 0000000000..c65835b610 --- /dev/null +++ b/apps/storybook/stories/cost-insights/Overview.stories.tsx @@ -0,0 +1,150 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/nextjs'; +import { + CostInsightsAskKiloView, + CostInsightsDashboardView, + CostInsightsShellView, + type CostInsightsDashboardData, + type CostInsightsPage, +} from '@/components/cost-insights'; +import { + anomalyAlert, + anomalyMetrics, + codingPlanSuggestion, + dashboardData, + emptyDashboardData, + evidenceAnomaly, + kiloPassSuggestion, + longLabelDrivers, + organizationOwner, + thresholdAlert, +} from './costInsightsFixtures'; + +const meta: Meta = { + title: 'Cost Insights/Overview', + component: CostInsightsDashboardView, + parameters: { layout: 'fullscreen' }, +}; + +export default meta; +type Story = StoryObj; + +function CostInsightsOverviewStory({ + data, + options = {}, + initialPage = 'dashboard', +}: { + data: CostInsightsDashboardData; + options?: { isLoading?: boolean; isError?: boolean; attention?: 'none' | 'alert' }; + initialPage?: CostInsightsPage; +}) { + const [activePage, setActivePage] = useState(initialPage); + const [askKiloQuestion, setAskKiloQuestion] = useState( + 'Create a graph of my costs for the last week' + ); + + function handleAskKilo(question: string) { + setAskKiloQuestion(question); + setActivePage('ask'); + } + + return ( + 0 ? 'alert' : 'none')} + onPageChange={setActivePage} + > + {activePage === 'ask' ? ( + + ) : ( + + )} + + ); +} + +function renderDashboard( + data: CostInsightsDashboardData, + options: { isLoading?: boolean; isError?: boolean; attention?: 'none' | 'alert' } = {} +) { + return ; +} + +export const PersonalOverview: Story = { + render: () => renderDashboard(dashboardData()), +}; + +export const AlertsNotSetUp: Story = { + render: () => + renderDashboard( + dashboardData({ + enabled: false, + alerts: [], + }) + ), +}; + +export const NoSpendYet: Story = { + render: () => renderDashboard(emptyDashboardData()), +}; + +export const AlertsNeedReview: Story = { + render: () => + renderDashboard( + dashboardData({ + alerts: [anomalyAlert, thresholdAlert], + metrics: anomalyMetrics(), + evidence: evidenceAnomaly, + }), + { attention: 'alert' } + ), +}; + +export const KiloPassSuggestion: Story = { + render: () => renderDashboard(dashboardData({ suggestions: [kiloPassSuggestion] })), +}; + +export const CodingPlanSuggestion: Story = { + render: () => renderDashboard(dashboardData({ suggestions: [codingPlanSuggestion] })), +}; + +export const AlertAndSuggestion: Story = { + render: () => + renderDashboard( + dashboardData({ + alerts: [anomalyAlert], + suggestions: [kiloPassSuggestion], + metrics: anomalyMetrics(), + evidence: evidenceAnomaly, + }), + { attention: 'alert' } + ), +}; + +export const Loading: Story = { + render: () => renderDashboard(dashboardData(), { isLoading: true }), +}; + +export const LoadError: Story = { + render: () => renderDashboard(dashboardData(), { isError: true }), +}; + +export const MobileOrganizationOverview: Story = { + render: () => + renderDashboard( + dashboardData({ + owner: organizationOwner, + drivers: longLabelDrivers, + memberLimitsHref: '/organizations/acme/members/limits', + }) + ), + globals: { + viewport: { value: 'mobile2', isRotated: false }, + }, +}; diff --git a/apps/storybook/stories/cost-insights/Settings.stories.tsx b/apps/storybook/stories/cost-insights/Settings.stories.tsx new file mode 100644 index 0000000000..2da18268e9 --- /dev/null +++ b/apps/storybook/stories/cost-insights/Settings.stories.tsx @@ -0,0 +1,85 @@ +import type { Meta, StoryObj } from '@storybook/nextjs'; +import { + CostInsightsSettingsView, + CostInsightsShellView, + type CostInsightsSettingsData, +} from '@/components/cost-insights'; +import { organizationOwner, personalOwner, settingsData } from './costInsightsFixtures'; + +const meta: Meta = { + title: 'Cost Insights/Alert Settings', + component: CostInsightsSettingsView, + parameters: { layout: 'fullscreen' }, +}; + +export default meta; +type Story = StoryObj; + +function renderSettings(data: CostInsightsSettingsData) { + return ( + + + + ); +} + +export const ThresholdConfigured: Story = { + render: () => renderSettings(settingsData()), +}; + +export const AlertsOffWithSavedThreshold: Story = { + render: () => + renderSettings( + settingsData({ + enabled: false, + thresholdUsd: '150.00', + saveState: 'dirty', + }) + ), +}; + +export const SuggestionsOff: Story = { + render: () => + renderSettings( + settingsData({ + suggestionsEnabled: false, + saveState: 'dirty', + }) + ), +}; + +export const InvalidThreshold: Story = { + render: () => + renderSettings( + settingsData({ + thresholdUsd: '10.125', + validations: ['Enter a threshold with no more than two decimal places.'], + saveState: 'dirty', + }) + ), +}; + +export const Saving: Story = { + render: () => renderSettings(settingsData({ saveState: 'saving' })), +}; + +export const SaveError: Story = { + render: () => renderSettings(settingsData({ saveState: 'error' })), +}; + +export const AdminReadOnly: Story = { + render: () => + renderSettings( + settingsData({ + owner: { ...organizationOwner, authorizedRole: 'admin' }, + readOnly: true, + }) + ), +}; + +export const Mobile: Story = { + render: () => renderSettings(settingsData({ owner: personalOwner, saveState: 'dirty' })), + globals: { + viewport: { value: 'mobile2', isRotated: false }, + }, +}; diff --git a/apps/storybook/stories/cost-insights/Shell.stories.tsx b/apps/storybook/stories/cost-insights/Shell.stories.tsx new file mode 100644 index 0000000000..9149f612a7 --- /dev/null +++ b/apps/storybook/stories/cost-insights/Shell.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/nextjs'; +import { CostInsightsShellView } from '@/components/cost-insights'; +import { orgMemberOwner } from './costInsightsFixtures'; + +const meta: Meta = { + title: 'Cost Insights/Access', + component: CostInsightsShellView, + parameters: { layout: 'fullscreen' }, +}; + +export default meta; +type Story = StoryObj; + +export const UnauthorizedOrganizationMember: Story = { + args: { + owner: orgMemberOwner, + activePage: 'dashboard', + unauthorized: true, + children: null, + }, +}; diff --git a/apps/storybook/stories/cost-insights/costInsightsFixtures.ts b/apps/storybook/stories/cost-insights/costInsightsFixtures.ts new file mode 100644 index 0000000000..6cb1739000 --- /dev/null +++ b/apps/storybook/stories/cost-insights/costInsightsFixtures.ts @@ -0,0 +1,475 @@ +import { Activity, AlertTriangle, CheckCircle2, DollarSign } from 'lucide-react'; +import { + type CostInsightsDashboardData, + type CostInsightsOwner, + type CostInsightsSettingsData, + type CostSuggestion, + type DashboardAlert, + type SpendDriver, + type SpendEvidencePoint, + type SpendMetric, + type CostInsightEvent, +} from '@/components/cost-insights'; + +const currencyFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 2, +}); + +const wholeDollarFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 0, +}); + +function money(value: number) { + return (value >= 100 ? wholeDollarFormatter : currencyFormatter).format(value); +} + +function buildSpendMetrics({ + currentHourUsd, + baselineUsd, + anomalyThresholdUsd, + rolling24hUsd, + thresholdUsd, +}: { + currentHourUsd: number; + baselineUsd: number; + anomalyThresholdUsd: number; + rolling24hUsd: number; + thresholdUsd?: number; +}): SpendMetric[] { + const remaining = thresholdUsd ? thresholdUsd - rolling24hUsd : undefined; + return [ + { + label: 'Total spend', + value: money(rolling24hUsd), + detail: 'Across all products', + tone: thresholdUsd && rolling24hUsd >= thresholdUsd ? 'warning' : 'neutral', + icon: DollarSign, + }, + { + label: 'Usage-based spend this hour', + value: money(currentHourUsd), + detail: + currentHourUsd >= anomalyThresholdUsd + ? 'Unusually high for this account' + : `Typical hour: ${money(baselineUsd)}`, + tone: currentHourUsd >= anomalyThresholdUsd ? 'warning' : 'neutral', + icon: Activity, + }, + { + label: '24-hour threshold', + value: thresholdUsd ? money(thresholdUsd) : 'Off', + detail: thresholdUsd + ? remaining !== undefined && remaining > 0 + ? `${money(remaining)} before alert` + : 'Threshold crossed' + : 'No threshold alert set', + tone: thresholdUsd && rolling24hUsd >= thresholdUsd ? 'warning' : 'neutral', + icon: AlertTriangle, + }, + { + label: 'Alert status', + value: thresholdUsd && rolling24hUsd >= thresholdUsd ? 'Review' : 'No alerts', + detail: thresholdUsd ? 'Spend Alerts are on' : 'Unusual spend alerts are on', + tone: thresholdUsd && rolling24hUsd >= thresholdUsd ? 'warning' : 'success', + icon: CheckCircle2, + }, + ]; +} + +export const personalOwner = { + type: 'personal', + name: 'Jean du Plessis', + authorizedRole: 'personal', +} satisfies CostInsightsOwner; + +export const organizationOwner = { + type: 'organization', + name: 'Acme Engineering', + authorizedRole: 'owner', +} satisfies CostInsightsOwner; + +export const orgMemberOwner = { + type: 'organization', + name: 'Acme Engineering', + authorizedRole: 'member', +} satisfies CostInsightsOwner; + +export const emptyMetrics: SpendMetric[] = buildSpendMetrics({ + currentHourUsd: 0, + baselineUsd: 0, + anomalyThresholdUsd: 25, + rolling24hUsd: 0, +}); + +export const evidence24h: SpendEvidencePoint[] = [ + { label: '00:00', variableUsd: 2.4, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '01:00', variableUsd: 3.1, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '02:00', variableUsd: 1.8, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '03:00', variableUsd: 0, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '04:00', variableUsd: 4.6, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '05:00', variableUsd: 6.2, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '06:00', variableUsd: 7.4, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '07:00', variableUsd: 8.1, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '08:00', variableUsd: 11.5, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '09:00', variableUsd: 13.2, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '10:00', variableUsd: 15.4, scheduledUsd: 12, anomalyThresholdUsd: 18 }, + { label: '11:00', variableUsd: 9.8, scheduledUsd: 0, anomalyThresholdUsd: 18 }, +]; + +export const evidenceAnomaly: SpendEvidencePoint[] = [ + ...evidence24h.slice(0, 8), + { label: '08:00', variableUsd: 19.25, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '09:00', variableUsd: 42.8, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: '10:00', variableUsd: 74.35, scheduledUsd: 0, anomalyThresholdUsd: 18 }, + { label: 'Now', variableUsd: 112.7, scheduledUsd: 0, anomalyThresholdUsd: 18 }, +]; + +export const evidence7d: SpendEvidencePoint[] = [ + { label: 'Thu', variableUsd: 42, scheduledUsd: 12, anomalyThresholdUsd: 48 }, + { label: 'Fri', variableUsd: 36, scheduledUsd: 0, anomalyThresholdUsd: 48 }, + { label: 'Sat', variableUsd: 12, scheduledUsd: 0, anomalyThresholdUsd: 48 }, + { label: 'Sun', variableUsd: 9, scheduledUsd: 0, anomalyThresholdUsd: 48 }, + { label: 'Mon', variableUsd: 51, scheduledUsd: 0, anomalyThresholdUsd: 48 }, + { label: 'Tue', variableUsd: 63, scheduledUsd: 0, anomalyThresholdUsd: 48 }, + { label: 'Wed', variableUsd: 44, scheduledUsd: 24, anomalyThresholdUsd: 48 }, +]; + +export const evidence30d: SpendEvidencePoint[] = Array.from({ length: 30 }, (_, index) => ({ + label: `Jun ${index + 1}`, + variableUsd: 18 + ((index * 13) % 47), + scheduledUsd: index % 7 === 2 ? 12 : 0, +})); + +export const evidence90d: SpendEvidencePoint[] = [ + { label: 'Mar 30', variableUsd: 118, scheduledUsd: 12 }, + { label: 'Apr 6', variableUsd: 142, scheduledUsd: 24 }, + { label: 'Apr 13', variableUsd: 97, scheduledUsd: 0 }, + { label: 'Apr 20', variableUsd: 166, scheduledUsd: 12 }, + { label: 'Apr 27', variableUsd: 154, scheduledUsd: 24 }, + { label: 'May 4', variableUsd: 189, scheduledUsd: 0 }, + { label: 'May 11', variableUsd: 203, scheduledUsd: 12 }, + { label: 'May 18', variableUsd: 171, scheduledUsd: 24 }, + { label: 'May 25', variableUsd: 214, scheduledUsd: 0 }, + { label: 'Jun 1', variableUsd: 226, scheduledUsd: 12 }, + { label: 'Jun 8', variableUsd: 198, scheduledUsd: 24 }, + { label: 'Jun 15', variableUsd: 241, scheduledUsd: 0 }, + { label: 'Jun 22', variableUsd: 186, scheduledUsd: 12 }, +]; + +export const personalDrivers: SpendDriver[] = [ + { + label: 'Kilo Code chat completions', + source: 'ai_gateway', + modelOrProvider: 'Claude Sonnet 4', + category: 'Variable Credit spend', + spendUsd: 56.2, + requestCount: 318, + }, + { + label: 'KiloClaw instance runtime', + source: 'kiloclaw', + modelOrProvider: 'openclaw-standard', + category: 'Scheduled Credit spend', + spendUsd: 12, + requestCount: 1, + }, + { + label: 'Coding Plan generation', + source: 'coding_plan', + modelOrProvider: 'OpenAI GPT-5', + category: 'Variable Credit spend', + spendUsd: 9.4, + requestCount: 17, + }, +]; + +export const organizationDrivers: SpendDriver[] = [ + { + label: 'Cloud Agent production incident workspace', + source: 'ai_gateway', + actorLabel: 'Maya Chen', + modelOrProvider: 'Claude Sonnet 4', + category: 'Variable Credit spend', + spendUsd: 181.4, + requestCount: 982, + href: '/organizations/acme/members/usr_01H7', + }, + { + label: 'KiloClaw hosted development environment', + source: 'kiloclaw', + actorLabel: 'Noah Williams', + modelOrProvider: 'openclaw-large', + category: 'Scheduled Credit spend', + spendUsd: 72, + requestCount: 3, + }, + { + label: 'Security remediation coding plan', + source: 'coding_plan', + actorLabel: 'Priya Shah', + modelOrProvider: 'OpenAI GPT-5', + category: 'Variable Credit spend', + spendUsd: 44.25, + requestCount: 73, + }, + { + label: 'Unknown metered tool usage', + source: 'other', + actorLabel: 'Jordan Lee', + category: 'Variable Credit spend', + spendUsd: 17.8, + requestCount: 42, + }, +]; + +export const longLabelDrivers: SpendDriver[] = [ + { + label: + 'Very long Cloud Agent session label from a repository migration with multiple production branches', + source: 'ai_gateway', + actorLabel: 'Deleted member', + modelOrProvider: 'Very long provider and model identifier with regional deployment suffix', + category: 'Variable Credit spend', + spendUsd: 412.99, + requestCount: 1204, + }, + ...organizationDrivers, +]; + +export const anomalyAlert = { + type: 'anomaly', + title: 'Spend is unusually high this hour', + description: "Usage-based spend is well above this account's recent hourly pattern.", + facts: [ + { label: 'This hour', value: '$112.70' }, + { label: 'Typical hour', value: '$6.00' }, + { label: 'Alert level', value: '$18.00' }, + ], + actions: ['acknowledge', 'view_spend'] as const, +} satisfies DashboardAlert; + +export const thresholdAlert = { + type: 'threshold', + title: '24-hour spend threshold crossed', + description: 'Spend reached $184.90 against the $150.00 threshold.', + facts: [ + { label: 'Last 24 hours', value: '$184.90' }, + { label: 'Threshold', value: '$150.00' }, + { label: 'Amount over', value: '$34.90' }, + ], + actions: ['acknowledge', 'adjust_threshold', 'disable_threshold'] as const, +} satisfies DashboardAlert; + +export const kiloPassSuggestion = { + id: 'suggestion-kilo-pass', + type: 'kilo_pass', + eyebrow: 'Cost suggestion', + title: 'Get more credits from your monthly spend with Kilo Pass Expert', + description: + 'You spent $106.90 on pay-as-you-go credits in the last 7 days. Kilo Pass Expert converts your $199 monthly payment into paid credits and lets you earn up to $79.60 more in free bonus credits.', + facts: [ + { label: 'Monthly payment', value: '$199' }, + { label: 'Paid credits', value: '$199' }, + { label: 'Potential bonus', value: '+$79.60' }, + ], + ctaLabel: 'View Kilo Pass Expert', + ctaHref: '/kilo-pass', +} satisfies CostSuggestion; + +export const codingPlanSuggestion = { + id: 'suggestion-minimax-plan', + type: 'coding_plan', + eyebrow: 'Cost suggestion', + title: 'Get more MiniMax usage with Token Plan Plus', + description: + 'You spent $15.00 on MiniMax in the last 7 days. For $20 every 30 days, Token Plan Plus includes about 1.7B M3 tokens and access to the full MiniMax model family.', + facts: [ + { label: 'Plan price', value: '$20' }, + { label: 'Included usage', value: '~1.7B tokens' }, + { label: 'Renews every', value: '30 days' }, + ], + ctaLabel: 'View MiniMax plan', + ctaHref: '/coding-plans/minimax', +} satisfies CostSuggestion; + +export const allEvents: CostInsightEvent[] = [ + { + id: 'evt-config', + type: 'config_changed', + title: 'Spend Alert settings changed', + description: '$150 spend threshold saved.', + timestampLabel: 'Today, 10:42', + actorLabel: 'Maya Chen', + }, + { + id: 'evt-anomaly', + type: 'anomaly_alert', + title: 'Spend Anomaly Alert created', + description: 'Current-hour Variable Credit spend crossed the anomaly threshold.', + timestampLabel: 'Today, 11:08', + amountLabel: '$112.70', + amountClassifier: 'current hour', + topDrivers: organizationDrivers, + }, + { + id: 'evt-threshold', + type: 'threshold_crossed', + title: 'Spend threshold crossed', + description: 'Rolling 24-hour Credit spend crossed $150.00.', + timestampLabel: 'Today, 11:12', + amountLabel: '$184.90', + amountClassifier: 'rolling 24h', + topDrivers: organizationDrivers, + }, + { + id: 'evt-suggestion-created', + type: 'suggestion_created', + title: 'Kilo Pass Expert suggested', + description: 'Recent pay-as-you-go spend indicated a Kilo Pass may improve cost efficiency.', + timestampLabel: 'Today, 11:20', + amountLabel: '$106.90', + amountClassifier: 'last 7 days', + }, + { + id: 'evt-suggestion-dismissed', + type: 'suggestion_dismissed', + title: 'MiniMax plan suggestion dismissed', + description: 'This suggestion is hidden until a materially new evaluation is available.', + timestampLabel: 'Today, 11:25', + actorLabel: 'Maya Chen', + }, + { + id: 'evt-review', + type: 'reviewed', + title: 'Spend threshold alert reviewed', + description: 'Manager acknowledged the alert and opened spend drivers.', + timestampLabel: 'Today, 11:31', + actorLabel: 'Priya Shah', + }, + { + id: 'evt-disabled', + type: 'disabled', + title: 'Spend Alerts disabled', + description: 'Spend Alerts stopped evaluating spend after explicit confirmation.', + timestampLabel: 'Yesterday, 19:04', + actorLabel: 'Maya Chen', + }, +]; + +export const longLabelEvents: CostInsightEvent[] = [ + { + id: 'evt-long', + type: 'anomaly_alert', + title: + 'Spend Anomaly Alert created for a long-running migration workspace with unusually long event metadata', + description: + 'Current-hour Variable Credit spend crossed the anomaly threshold with long model, provider, product, and actor labels.', + timestampLabel: 'Today, 11:58', + amountLabel: '$1,204.18', + amountClassifier: 'current hour', + actorLabel: 'Deleted member', + topDrivers: longLabelDrivers, + }, + ...allEvents, +]; + +export function dashboardData( + overrides: Partial = {} +): CostInsightsDashboardData { + return { + enabled: true, + owner: personalOwner, + range: '24h', + metrics: buildSpendMetrics({ + currentHourUsd: 15.4, + baselineUsd: 6, + anomalyThresholdUsd: 18, + rolling24hUsd: 74.25, + thresholdUsd: 150, + }), + evidence: evidence24h, + evidenceByRange: { + '24h': evidence24h, + '7d': evidence7d, + '30d': evidence30d, + '90d': evidence90d, + }, + drivers: personalDrivers, + alerts: [], + suggestions: [], + lastEvaluatedLabel: 'Evaluated 2 minutes ago', + baselineMode: 'seven-day', + eventPreview: allEvents, + ...overrides, + }; +} + +export function emptyDashboardData( + overrides: Partial = {} +): CostInsightsDashboardData { + return dashboardData({ + metrics: emptyMetrics, + evidence: [], + drivers: [], + eventPreview: [], + baselineMode: 'starter', + lastEvaluatedLabel: 'No evaluation yet', + ...overrides, + }); +} + +export function anomalyMetrics() { + return buildSpendMetrics({ + currentHourUsd: 112.7, + baselineUsd: 6, + anomalyThresholdUsd: 18, + rolling24hUsd: 184.9, + thresholdUsd: 150, + }); +} + +export function thresholdMetrics() { + return buildSpendMetrics({ + currentHourUsd: 12.8, + baselineUsd: 6, + anomalyThresholdUsd: 18, + rolling24hUsd: 184.9, + thresholdUsd: 150, + }); +} + +export function settingsData( + overrides: Partial = {} +): CostInsightsSettingsData { + return { + owner: personalOwner, + enabled: true, + suggestionsEnabled: true, + thresholdUsd: '150.00', + saveState: 'saved', + ...overrides, + }; +} + +const thresholdStatusMetric = { + label: 'Spend threshold', + value: 'Crossed', + detail: 'Review current episode', + tone: 'warning', + icon: AlertTriangle, +} satisfies SpendMetric; + +export const thresholdOnlyMetrics: SpendMetric[] = [ + ...buildSpendMetrics({ + currentHourUsd: 12.8, + baselineUsd: 5, + anomalyThresholdUsd: 15, + rolling24hUsd: 151.4, + thresholdUsd: 150, + }).slice(0, 3), + thresholdStatusMetric, +]; diff --git a/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx b/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx index ae45076ae7..3c8afb2938 100644 --- a/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx +++ b/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx @@ -27,6 +27,7 @@ import { MessageSquare, ChevronLeft, ChevronRight, + ChartLine, } from 'lucide-react'; import { usePathname } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; @@ -99,6 +100,8 @@ export default function OrganizationAppSidebar({ } }, [actualRole, user?.is_admin, setOriginalRole, setAssumedRole]); + const hasOwnerLevelAccess = currentRole === 'owner' || currentRole === 'billing_manager'; + // Dashboard group const dashboardItems: Array<{ title: string; @@ -125,10 +128,17 @@ export default function OrganizationAppSidebar({ icon: ChartColumnIncreasing, url: `/organizations/${organizationId}/usage-details`, }, + ...(hasOwnerLevelAccess + ? [ + { + title: 'Cost Insights', + icon: ChartLine, + url: `/organizations/${organizationId}/cost-insights`, + }, + ] + : []), ]; - const hasOwnerLevelAccess = currentRole === 'owner' || currentRole === 'billing_manager'; - // KiloClaw group const kiloClawItems: Array<{ title: string; diff --git a/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx b/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx index 7a57a915eb..9d96925668 100644 --- a/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx +++ b/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx @@ -33,6 +33,7 @@ import { Gift, ChevronLeft, ChevronRight, + ChartLine, } from 'lucide-react'; import HeaderLogo from '@/components/HeaderLogo'; import OrganizationSwitcher from './OrganizationSwitcher'; @@ -87,6 +88,11 @@ export default function PersonalAppSidebar(props: React.ComponentProps(initialFilter); + const [page, setPage] = useState(initialPage); + + if (isLoading) return ; + if (isError) return ; + + const filteredEvents = events.filter(event => { + if (filter === 'alerts') return ['anomaly_alert', 'threshold_crossed'].includes(event.type); + if (filter === 'suggestions') + return ['suggestion_created', 'suggestion_dismissed'].includes(event.type); + if (filter === 'reviews') return event.type === 'reviewed'; + if (filter === 'settings') return ['config_changed', 'disabled'].includes(event.type); + return true; + }); + const pageCount = Math.max(1, Math.ceil(filteredEvents.length / ACTIVITY_PAGE_SIZE)); + const currentPage = Math.min(page, pageCount); + const pageEvents = filteredEvents.slice( + (currentPage - 1) * ACTIVITY_PAGE_SIZE, + currentPage * ACTIVITY_PAGE_SIZE + ); + const firstResult = filteredEvents.length === 0 ? 0 : (currentPage - 1) * ACTIVITY_PAGE_SIZE + 1; + const lastResult = Math.min(currentPage * ACTIVITY_PAGE_SIZE, filteredEvents.length); + + return ( + + +
+
+ + +
+ + {filteredEvents.length === 0 + ? 'No matching activity' + : `Showing ${firstResult}-${lastResult} of ${filteredEvents.length}`} + +
+
+ + {empty || events.length === 0 ? ( +
+ +
+ ) : pageEvents.length === 0 ? ( +
+ +
+ ) : ( + + )} +
+ {pageCount > 1 && ( +
+ +
+ )} +
+ ); +} diff --git a/apps/web/src/components/cost-insights/activity/EventList.tsx b/apps/web/src/components/cost-insights/activity/EventList.tsx new file mode 100644 index 0000000000..c29b80115d --- /dev/null +++ b/apps/web/src/components/cost-insights/activity/EventList.tsx @@ -0,0 +1,168 @@ +import { + AlertTriangle, + CheckCircle2, + ChevronDown, + Lightbulb, + Settings2, + TrendingUp, + XCircle, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { money, percentOf, sourceLabels } from '../formatting'; +import { EmptyPanel } from '../shared/EmptyPanel'; +import { StatusBadge } from '../shared/StatusBadge'; +import type { CostInsightEvent } from '../types'; + +export function EventList({ + events, + compact = false, +}: { + events: CostInsightEvent[]; + compact?: boolean; +}) { + if (events.length === 0) + return ; + return ( +
    + {events.map(event => { + const Icon = + event.type === 'anomaly_alert' + ? TrendingUp + : event.type === 'threshold_crossed' + ? AlertTriangle + : event.type === 'suggestion_created' + ? Lightbulb + : event.type === 'reviewed' + ? CheckCircle2 + : event.type === 'suggestion_dismissed' || event.type === 'disabled' + ? XCircle + : Settings2; + const eventLabel = + event.type === 'anomaly_alert' + ? 'Anomaly alert' + : event.type === 'threshold_crossed' + ? 'Threshold alert' + : event.type === 'suggestion_created' + ? 'Suggestion' + : event.type === 'suggestion_dismissed' + ? 'Suggestion dismissed' + : event.type === 'reviewed' + ? 'Review' + : 'Settings change'; + const capturedSpend = + event.topDrivers?.reduce((sum, driver) => sum + driver.spendUsd, 0) ?? 0; + return ( +
  1. +
    +
    + + + {eventLabel} + +
    +
    +
    +
    +
    +
    +
    {event.title}
    +

    {event.description}

    + {event.actorLabel && ( +

    By {event.actorLabel}

    + )} +
    +
    +
    + {event.amountLabel && ( +
    +
    + {event.amountLabel} +
    + {event.amountClassifier && ( +
    + {event.amountClassifier} +
    + )} +
    + )} + {!compact && event.topDrivers && event.topDrivers.length > 0 && ( +
    + + View contributors + +
    +
    +
    + Largest contributors at alert time +
    +
    + {money(capturedSpend)} captured +
    +
    +
      + {event.topDrivers.slice(0, 5).map((driver, index) => { + const share = percentOf(driver.spendUsd, capturedSpend); + return ( +
    1. + + {index + 1} + +
      +
      + {driver.label} +
      +
      + {driver.actorLabel ?? 'No member attributed'} + {sourceLabels[driver.source]} + {driver.modelOrProvider && {driver.modelOrProvider}} +
      +
      +
      +
      + {money(driver.spendUsd)} +
      +
      + {share}% of captured spend +
      + +
    2. + ); + })} +
    +
    +
    + )} +
    +
  2. + ); + })} +
+ ); +} diff --git a/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx b/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx new file mode 100644 index 0000000000..60ff5c42a5 --- /dev/null +++ b/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx @@ -0,0 +1,177 @@ +'use client'; + +import { useState, type FormEvent } from 'react'; +import { BarChart3, ChevronDown, ChevronUp, Send } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; + +const askKiloChartData = [ + { date: 'Jun 18', cost: 1.42, color: 'var(--chart-1)' }, + { date: 'Jun 19', cost: 0.28, color: 'var(--chart-2)' }, + { date: 'Jun 20', cost: 0.17, color: 'var(--chart-3)' }, + { date: 'Jun 21', cost: 0, color: 'var(--chart-4)' }, + { date: 'Jun 22', cost: 0, color: 'var(--chart-5)' }, + { date: 'Jun 23', cost: 0.45, color: 'var(--chart-1)' }, + { date: 'Jun 24', cost: 0.31, color: 'var(--chart-2)' }, +]; + +export function CostInsightsAskKiloView({ + initialQuestion = 'Create a graph of my costs for the last week', +}: { + initialQuestion?: string; +}) { + const [question, setQuestion] = useState(''); + const [messages, setMessages] = useState([{ id: 'initial', question: initialQuestion }]); + const [chartExpanded, setChartExpanded] = useState(true); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + const trimmedQuestion = question.trim(); + if (!trimmedQuestion) return; + setMessages(currentMessages => [ + ...currentMessages, + { id: `question-${currentMessages.length}`, question: trimmedQuestion }, + ]); + setQuestion(''); + } + + return ( +
+
+ {messages.map(message => ( +
+
+ {message.question} +
+
+
+ + {chartExpanded && ( +
+

+ Model usage · Cost · Jun 18, 2026 to Jun 24, 2026 +

+

Cost by date

+
+
+ Daily cost from June 18 to June 24. Peak cost was $1.42 on June 18. No spend + occurred June 21 or June 22. +
+
+
+ {[1.6, 1.2, 0.8, 0.4, 0].map(value => ( + ${value.toFixed(2)} + ))} +
+
+ + {askKiloChartData.map(item => ( +
+
+ + {item.date.replace('Jun ', '')} + +
+ ))} +
+
+
+ Jun 18–24 +
+
+
+ )} +
+ +
+

Here is your daily cost trend for the last 7 days (Jun 18–24):

+
    +
  • + Total spend: $2.63 over the week +
  • +
  • + Daily average: $0.38 +
  • +
  • + Peak day: Jun 18 at $1.42, 54% of + the week's cost +
  • +
  • + Quietest days: Jun 21–22 with no + Credit spend +
  • +
  • + Trend: Spend peaked at the start of + the week, paused midweek, then resumed at a lower level. +
  • +
+

+ The Jun 18 spike is the main driver of weekly spend. Break down that day by model + to identify which usage drove the cost. +

+
+
+
+ ))} +
+ +
+ +
+ setQuestion(event.target.value)} + placeholder="Ask a follow-up about your spending..." + className="bg-card h-12! rounded-xl pr-14 shadow-lg" + /> + +
+
+
+ ); +} diff --git a/apps/web/src/components/cost-insights/formatting.ts b/apps/web/src/components/cost-insights/formatting.ts new file mode 100644 index 0000000000..01a7a573eb --- /dev/null +++ b/apps/web/src/components/cost-insights/formatting.ts @@ -0,0 +1,29 @@ +import type { SpendDriver } from './types'; + +const currencyFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 2, +}); + +const wholeDollarFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 0, +}); + +export const sourceLabels = { + ai_gateway: 'AI usage', + kiloclaw: 'KiloClaw', + coding_plan: 'Coding Plan', + other: 'Other', +} satisfies Record; + +export function money(value: number) { + return (value >= 100 ? wholeDollarFormatter : currencyFormatter).format(value); +} + +export function percentOf(value: number, total: number) { + if (total <= 0) return 0; + return Math.round((value / total) * 100); +} diff --git a/apps/web/src/components/cost-insights/index.ts b/apps/web/src/components/cost-insights/index.ts new file mode 100644 index 0000000000..7439ece667 --- /dev/null +++ b/apps/web/src/components/cost-insights/index.ts @@ -0,0 +1,25 @@ +export type { + ActivityFilter, + AlertFact, + CostInsightEvent, + CostInsightEventType, + CostInsightsAttention, + CostInsightsDashboardData, + CostInsightsOwner, + CostInsightsPage, + CostInsightsSettingsData, + CostSuggestion, + DashboardAlert, + DashboardAlertAction, + SettingsConfirmation, + SpendDriver, + SpendEvidencePoint, + SpendMetric, + SpendRange, +} from './types'; +export { CostInsightsAlertBar } from './shell/CostInsightsAlertBar'; +export { CostInsightsShellView } from './shell/CostInsightsShellView'; +export { CostInsightsDashboardView } from './overview/CostInsightsDashboardView'; +export { CostInsightsAskKiloView } from './ask-kilo/CostInsightsAskKiloView'; +export { CostInsightsSettingsView } from './settings/CostInsightsSettingsView'; +export { CostInsightsEventHistoryView } from './activity/CostInsightsEventHistoryView'; diff --git a/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx b/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx new file mode 100644 index 0000000000..ea34e3fbc7 --- /dev/null +++ b/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useState, type FormEvent } from 'react'; +import { Send } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import type { CostInsightsOwner } from '../types'; + +export function AskKiloInput({ + owner, + onSubmit, +}: { + owner: CostInsightsOwner; + onSubmit?: (question: string) => void; +}) { + const [question, setQuestion] = useState(''); + const askHref = + owner.type === 'organization' + ? '/organizations/acme-cost-insights/cost-insights?tab=ask' + : '/cost-insights?tab=ask'; + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + const trimmedQuestion = question.trim(); + if (!trimmedQuestion) return; + + if (onSubmit) { + onSubmit(trimmedQuestion); + return; + } + + window.location.assign(askHref); + } + + return ( +
+ + setQuestion(event.target.value)} + placeholder="Ask Kilo any question about your spending..." + className="bg-card h-12! rounded-xl pr-14" + /> + +
+ ); +} diff --git a/apps/web/src/components/cost-insights/overview/CostInsightsDashboardView.tsx b/apps/web/src/components/cost-insights/overview/CostInsightsDashboardView.tsx new file mode 100644 index 0000000000..66818ea0d7 --- /dev/null +++ b/apps/web/src/components/cost-insights/overview/CostInsightsDashboardView.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { CheckCircle2 } from 'lucide-react'; +import { Skeleton } from '@/components/ui/skeleton'; +import { CostInsightsLoadError } from '../shared/CostInsightsLoadError'; +import { StatusBadge } from '../shared/StatusBadge'; +import type { CostInsightsDashboardData, SpendMetric } from '../types'; +import { AskKiloInput } from './AskKiloInput'; +import { DisabledAlertsBanner, ReviewBanner, SuggestionCard } from './DashboardNotices'; +import { EventPreviewCard } from './EventPreviewCard'; +import { SpendEvidenceCard } from './SpendEvidenceCard'; +import { TopDriversCard } from './TopDriversCard'; +import { cn } from '@/lib/utils'; + +const toneClasses = { + neutral: 'text-foreground', + success: 'text-status-success', + warning: 'text-status-warning', + danger: 'text-status-destructive', +} satisfies Record; + +export function CostInsightsDashboardView({ + data, + isLoading = false, + isError = false, + onAskKilo, +}: { + data: CostInsightsDashboardData; + isLoading?: boolean; + isError?: boolean; + onAskKilo?: (question: string) => void; +}) { + if (isLoading) return ; + if (isError) return ; + + return ( +
+ + {data.alerts.map((alert, index) => ( + + ))} + {data.suggestions.map(suggestion => ( + + ))} + {!data.enabled && } + +
+
+
+

+ Last 24 hours +

+

+ Spend charged to {data.owner.name}. +

+
+ {data.enabled && data.alerts.length === 0 && ( + + + )} +
+
+ {data.metrics.map(metric => ( + + ))} +
+
+ +
+ + +
+ + +
+ ); +} + +function DashboardSkeleton() { + return ( + + +
+ {[0, 1, 2, 3].map(index => ( + + ))} +
+
+ + +
+
+ ); +} + +function MetricTile({ metric }: { metric: SpendMetric }) { + const Icon = metric.icon; + return ( +
+
+
+
+ {metric.value} +
+

{metric.detail}

+
+ ); +} diff --git a/apps/web/src/components/cost-insights/overview/DashboardNotices.tsx b/apps/web/src/components/cost-insights/overview/DashboardNotices.tsx new file mode 100644 index 0000000000..f9db309525 --- /dev/null +++ b/apps/web/src/components/cost-insights/overview/DashboardNotices.tsx @@ -0,0 +1,160 @@ +import { AlertTriangle, ArrowRight, Bell, Lightbulb, TrendingUp, XCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import type { CostSuggestion, DashboardAlert, DashboardAlertAction } from '../types'; + +const reviewActionLabels = { + acknowledge: 'Mark as reviewed', + view_spend: 'View spend drivers', + disable_alerts: 'Turn off alerts', + adjust_threshold: 'Change threshold', + disable_threshold: 'Turn off threshold', +} satisfies Record; + +export function DisabledAlertsBanner() { + return ( +
+
+
+

+ Get notified about unexpected spend +

+

+ Spend data stays visible. Turn on Spend Alerts for unusual hourly increases and an + optional 24-hour threshold. +

+
+ +
+
+ ); +} + +export function ReviewBanner({ + alert, + primaryAction, +}: { + alert: DashboardAlert; + primaryAction: boolean; +}) { + const Icon = alert.type === 'threshold' ? AlertTriangle : TrendingUp; + return ( +
+
+
+
+
+ {alert.facts && ( +
+ {alert.facts.map(fact => ( +
+
{fact.label}
+
+ {fact.value} +
+
+ ))} +
+ )} +
+ +
+
+ ); +} + +function ReviewActions({ + alert, + primaryAction, +}: { + alert: DashboardAlert; + primaryAction: boolean; +}) { + return ( +
+ {alert.actions.map((action, index) => ( + + ))} +
+ ); +} + +export function SuggestionCard({ suggestion }: { suggestion: CostSuggestion }) { + return ( +
+
+
+
+
+
+ {suggestion.facts.map(fact => ( +
+
{fact.label}
+
+ {fact.value} +
+
+ ))} +
+

+ Benefits shown use current plan terms. Actual value depends on usage and eligibility. +

+
+
+ + +
+
+
+ ); +} diff --git a/apps/web/src/components/cost-insights/overview/EventPreviewCard.tsx b/apps/web/src/components/cost-insights/overview/EventPreviewCard.tsx new file mode 100644 index 0000000000..34e95f5e82 --- /dev/null +++ b/apps/web/src/components/cost-insights/overview/EventPreviewCard.tsx @@ -0,0 +1,25 @@ +import { History } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { EventList } from '../activity/EventList'; +import type { CostInsightEvent } from '../types'; + +export function EventPreviewCard({ events }: { events: CostInsightEvent[] }) { + return ( + + +
+ Recent activity + Alerts, suggestions, reviews, and settings changes. +
+ +
+ + + +
+ ); +} diff --git a/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx b/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx new file mode 100644 index 0000000000..2bbca2660d --- /dev/null +++ b/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx @@ -0,0 +1,219 @@ +'use client'; + +import { useState, type CSSProperties } from 'react'; +import { ArrowRight, Clock3 } from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { money, percentOf } from '../formatting'; +import { EmptyPanel } from '../shared/EmptyPanel'; +import type { CostInsightsDashboardData, SpendRange } from '../types'; + +function isSpendRange(value: string): value is SpendRange { + return ['24h', '7d', '30d', '90d'].includes(value); +} + +export function SpendEvidenceCard({ data }: { data: CostInsightsDashboardData }) { + const [selectedRange, setSelectedRange] = useState(); + const range = selectedRange ?? data.range; + const evidence = range === data.range ? data.evidence : (data.evidenceByRange?.[range] ?? []); + const totals = evidence.map(point => point.variableUsd + point.scheduledUsd); + const maxSpend = Math.max(1, ...totals); + const rangeLabel = { + '24h': 'Last 24 hours', + '7d': 'Last 7 days', + '30d': 'Last 30 days', + '90d': 'Last 90 days', + }[range]; + const highestIndex = totals.indexOf(Math.max(...totals)); + const highest = evidence[highestIndex]; + const total = totals.reduce((sum, value) => sum + value, 0); + const isDenseRange = range === '30d'; + const barMinimumWidth = range === '30d' ? '0.75rem' : range === '90d' ? '1.5rem' : '2rem'; + const chartMinimumWidth = range === '30d' ? '40rem' : '32rem'; + + return ( + + +
+ Spend over time + + {rangeLabel}. Usage-based and scheduled spend are shown separately. + +
+ { + if (isSpendRange(value)) setSelectedRange(value); + }} + > + + {(['24h', '7d', '30d', '90d'] as SpendRange[]).map(range => ( + + {range} + + ))} + + +
+ + {evidence.length === 0 ? ( + + ) : ( + <> + +

+ {rangeLabel}: {money(total)} total.{' '} + {highest + ? `Highest period was ${highest.label} at ${money(highest.variableUsd + highest.scheduledUsd)}.` + : ''} +

+
+
+ + {evidence.map((point, index) => { + const pointTotal = point.variableUsd + point.scheduledUsd; + const totalHeight = Math.max(2, percentOf(pointTotal, maxSpend)); + const scheduledShare = percentOf(point.scheduledUsd, pointTotal); + return ( + + + + + +
{point.label}
+
+
Total
+
+ {money(pointTotal)} +
+
+
+
+ {money(point.variableUsd)} +
+
+
+
+ {money(point.scheduledUsd)} +
+
+
+
+ ); + })} +
+
+
+ Scroll chart to see all periods +
+
+ + + {baselineLabel(data.baselineMode)} +
+ + )} +
+
+ ); +} + +function ChartGridLine({ position, label }: { position: string; label: string }) { + return ( +
+ + {label} + +
+ ); +} + +function baselineLabel(mode: CostInsightsDashboardData['baselineMode']) { + if (mode === 'starter') return 'Anomaly detection uses a starter alert level'; + if (mode === 'available-history') return 'Anomaly detection uses available spend history'; + return 'Anomaly detection uses your recent hourly pattern'; +} diff --git a/apps/web/src/components/cost-insights/overview/TopDriversCard.tsx b/apps/web/src/components/cost-insights/overview/TopDriversCard.tsx new file mode 100644 index 0000000000..51ac35d724 --- /dev/null +++ b/apps/web/src/components/cost-insights/overview/TopDriversCard.tsx @@ -0,0 +1,127 @@ +import { UsersRound } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { cn } from '@/lib/utils'; +import { money, percentOf, sourceLabels } from '../formatting'; +import { EmptyPanel } from '../shared/EmptyPanel'; +import type { CostInsightsOwner, SpendDriver } from '../types'; + +export function TopDriversCard({ + drivers, + owner, + memberLimitsHref, +}: { + drivers: SpendDriver[]; + owner: CostInsightsOwner; + memberLimitsHref?: string; +}) { + const total = drivers.reduce((sum, driver) => sum + driver.spendUsd, 0); + return ( + + + Where spend went + Largest contributors in the selected period. + + + {drivers.length === 0 ? ( + + ) : ( +
    + {drivers.slice(0, 5).map(driver => ( +
  1. + +
  2. + ))} +
+ )} + {owner.type === 'organization' && memberLimitsHref && ( + + )} +
+
+ ); +} + +function DriverRow({ + driver, + total, + showMember, +}: { + driver: SpendDriver; + total: number; + showMember: boolean; +}) { + const row = ( +
+
+
+
{driver.label}
+
+
+
Product
+
{sourceLabels[driver.source]}
+
+ {showMember && ( +
+
Member
+
{driver.actorLabel ?? 'No member attributed'}
+
+ )} + {driver.modelOrProvider && ( +
+
Model or provider
+
{driver.modelOrProvider}
+
+ )} +
+
+
+
+ {money(driver.spendUsd)} +
+
+ {percentOf(driver.spendUsd, total)}% of shown spend +
+
+
+ + ); + return driver.href ? ( + + {row} + + ) : ( + row + ); +} diff --git a/apps/web/src/components/cost-insights/settings/CostInsightsSettingsView.tsx b/apps/web/src/components/cost-insights/settings/CostInsightsSettingsView.tsx new file mode 100644 index 0000000000..92e23bbb2b --- /dev/null +++ b/apps/web/src/components/cost-insights/settings/CostInsightsSettingsView.tsx @@ -0,0 +1,189 @@ +'use client'; + +import { AlertCircle, Loader2, Lock, Save, TrendingUp } from 'lucide-react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { cn } from '@/lib/utils'; +import type { CostInsightsSettingsData } from '../types'; + +export function CostInsightsSettingsView({ data }: { data: CostInsightsSettingsData }) { + const validation = data.validations?.[0]; + const saveLabel = data.saveState === 'saving' ? 'Saving changes...' : 'Save changes'; + return ( +
+ {data.readOnly && ( + + + )} + {data.saveState === 'error' && ( + + + )} + + + +
+
+

+ Cost Suggestions +

+

+ Get email and in-app recommendations when a Coding Plan or Kilo Pass may make your + usage more cost-efficient. Suggestions are on by default and do not change billing + automatically. +

+
+
+ + +
+
+ +
+
+

+ Spend Alerts +

+

+ Get email and in-app alerts when hourly spend is unusually high or your 24-hour + threshold is crossed. +

+
+
+ + +
+
+ +
+
+
+
+ +
+
+
+

+ 24-hour spend threshold +

+

+ Optional. Includes all Credit spend in a rolling 24-hour period. +

+
+
+ +
+ + +
+

+ Leave blank to turn off threshold alerts. You can save this amount while Spend + Alerts are off. +

+ {validation && ( +

+ {validation} +

+ )} +
+
+
+
+
+ + {!data.readOnly && ( +
+ + {data.saveState === 'saved' + ? 'All changes saved' + : data.saveState === 'dirty' + ? 'Unsaved changes' + : data.saveState === 'error' + ? 'Save failed' + : 'Saving changes...'} + + +
+ )} +
+ ); +} diff --git a/apps/web/src/components/cost-insights/shared/CostInsightsLoadError.tsx b/apps/web/src/components/cost-insights/shared/CostInsightsLoadError.tsx new file mode 100644 index 0000000000..cb444fa0bc --- /dev/null +++ b/apps/web/src/components/cost-insights/shared/CostInsightsLoadError.tsx @@ -0,0 +1,23 @@ +import { AlertCircle, RefreshCw } from 'lucide-react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; + +export function CostInsightsLoadError() { + return ( + + + ); +} diff --git a/apps/web/src/components/cost-insights/shared/EmptyPanel.tsx b/apps/web/src/components/cost-insights/shared/EmptyPanel.tsx new file mode 100644 index 0000000000..0b7e41b42f --- /dev/null +++ b/apps/web/src/components/cost-insights/shared/EmptyPanel.tsx @@ -0,0 +1,8 @@ +export function EmptyPanel({ title, description }: { title: string; description: string }) { + return ( +
+
{title}
+

{description}

+
+ ); +} diff --git a/apps/web/src/components/cost-insights/shared/StatusBadge.tsx b/apps/web/src/components/cost-insights/shared/StatusBadge.tsx new file mode 100644 index 0000000000..e54b44f2e8 --- /dev/null +++ b/apps/web/src/components/cost-insights/shared/StatusBadge.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from 'react'; +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; +import type { SpendMetric } from '../types'; + +const statusBadgeClasses = { + neutral: 'border-border text-muted-foreground', + success: 'border-status-success-border text-status-success', + warning: 'border-status-warning-border text-status-warning', + danger: 'border-status-destructive-border text-status-destructive', +} satisfies Record; + +export function StatusBadge({ + children, + tone = 'neutral', +}: { + children: ReactNode; + tone?: SpendMetric['tone']; +}) { + return ( + + {children} + + ); +} diff --git a/apps/web/src/components/cost-insights/shell/CostInsightsAlertBar.tsx b/apps/web/src/components/cost-insights/shell/CostInsightsAlertBar.tsx new file mode 100644 index 0000000000..8c9e4c08bc --- /dev/null +++ b/apps/web/src/components/cost-insights/shell/CostInsightsAlertBar.tsx @@ -0,0 +1,39 @@ +import { AlertTriangle, ArrowRight } from 'lucide-react'; +import { Banner } from '@/components/shared/Banner'; +import type { CostInsightsOwner } from '../types'; + +export function CostInsightsAlertBar({ + owner, + alertCount, +}: { + owner: CostInsightsOwner; + alertCount: number; +}) { + const reviewHref = + owner.type === 'organization' + ? '/organizations/acme-cost-insights/cost-insights' + : '/cost-insights'; + const alertLabel = + alertCount === 1 ? 'Spend Alert needs review' : `${alertCount} Spend Alerts need review`; + + return ( + + + + + {alertLabel} + Review unexpected spend for {owner.name}. + + + + Review spend + + + + ); +} diff --git a/apps/web/src/components/cost-insights/shell/CostInsightsShellView.tsx b/apps/web/src/components/cost-insights/shell/CostInsightsShellView.tsx new file mode 100644 index 0000000000..422e51ceda --- /dev/null +++ b/apps/web/src/components/cost-insights/shell/CostInsightsShellView.tsx @@ -0,0 +1,149 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { DollarSign, Lock, UserRound, UsersRound } from 'lucide-react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { cn } from '@/lib/utils'; +import { StatusBadge } from '../shared/StatusBadge'; +import type { CostInsightsAttention, CostInsightsOwner, CostInsightsPage } from '../types'; + +export function CostInsightsShellView({ + owner, + activePage, + attention = 'none', + unauthorized = false, + mobilePreview = false, + onPageChange, + children, +}: { + owner: CostInsightsOwner; + activePage: CostInsightsPage; + attention?: CostInsightsAttention; + unauthorized?: boolean; + mobilePreview?: boolean; + onPageChange?: (page: CostInsightsPage) => void; + children: ReactNode; +}) { + const basePath = + owner.type === 'organization' + ? '/organizations/acme-cost-insights/cost-insights' + : '/cost-insights'; + const navItems = [ + { page: 'dashboard' as const, label: 'Overview', href: basePath }, + { page: 'ask' as const, label: 'Ask Kilo', href: `${basePath}?tab=ask` }, + { page: 'events' as const, label: 'Activity', href: `${basePath}?tab=events` }, + { page: 'settings' as const, label: 'Alert settings', href: `${basePath}/config` }, + ]; + const roleLabel = + owner.authorizedRole === 'billing_manager' + ? 'Billing manager' + : owner.authorizedRole === 'owner' + ? 'Organization owner' + : owner.authorizedRole === 'admin' + ? 'Admin view' + : 'Personal account'; + + if (unauthorized) { + return ( +
+ + +
+ ); + } + + return ( +
+
+ + +
+
+
+
+
+

Cost Insights

+

+ See what is driving Credit spend and get notified when spending changes. +

+
+
+ {owner.name} + + {owner.type === 'organization' ? ( + +
+
+
+
+ + + +
+ {children} +
+
+
+
+ ); +} diff --git a/apps/web/src/components/cost-insights/types.ts b/apps/web/src/components/cost-insights/types.ts new file mode 100644 index 0000000000..995342b3fe --- /dev/null +++ b/apps/web/src/components/cost-insights/types.ts @@ -0,0 +1,122 @@ +import type { LucideIcon } from 'lucide-react'; + +export type CostInsightsOwner = { + type: 'personal' | 'organization'; + name: string; + authorizedRole?: 'personal' | 'owner' | 'billing_manager' | 'member' | 'admin'; +}; + +export type CostInsightsPage = 'dashboard' | 'ask' | 'settings' | 'events'; +export type CostInsightsAttention = 'none' | 'alert'; +export type SpendRange = '24h' | '7d' | '30d' | '90d'; + +export type SpendMetric = { + label: string; + value: string; + detail: string; + tone: 'neutral' | 'success' | 'warning' | 'danger'; + icon: LucideIcon; +}; + +export type SpendEvidencePoint = { + label: string; + variableUsd: number; + scheduledUsd: number; + anomalyThresholdUsd?: number; +}; + +export type SpendDriver = { + label: string; + source: 'ai_gateway' | 'kiloclaw' | 'coding_plan' | 'other'; + actorLabel?: string; + modelOrProvider?: string; + category: 'Variable Credit spend' | 'Scheduled Credit spend'; + spendUsd: number; + requestCount: number; + href?: string; +}; + +export type AlertFact = { label: string; value: string }; + +export type DashboardAlert = + | { + type: 'anomaly'; + title: string; + description: string; + facts?: AlertFact[]; + actions: ('acknowledge' | 'view_spend' | 'disable_alerts')[]; + } + | { + type: 'threshold'; + title: string; + description: string; + facts?: AlertFact[]; + actions: ('acknowledge' | 'adjust_threshold' | 'disable_threshold')[]; + }; + +export type DashboardAlertAction = DashboardAlert['actions'][number]; + +export type CostSuggestion = { + id: string; + type: 'coding_plan' | 'kilo_pass'; + eyebrow: string; + title: string; + description: string; + facts: AlertFact[]; + ctaLabel: string; + ctaHref: string; +}; + +export type CostInsightsDashboardData = { + enabled: boolean; + owner: CostInsightsOwner; + range: SpendRange; + metrics: SpendMetric[]; + evidence: SpendEvidencePoint[]; + evidenceByRange?: Partial>; + drivers: SpendDriver[]; + alerts: DashboardAlert[]; + suggestions: CostSuggestion[]; + lastEvaluatedLabel: string; + baselineMode: 'starter' | 'available-history' | 'seven-day'; + eventPreview: CostInsightEvent[]; + memberLimitsHref?: string; +}; + +export type CostInsightEventType = + | 'config_changed' + | 'anomaly_alert' + | 'threshold_crossed' + | 'reviewed' + | 'suggestion_created' + | 'suggestion_dismissed' + | 'disabled'; + +export type CostInsightEvent = { + id: string; + type: CostInsightEventType; + title: string; + description: string; + timestampLabel: string; + actorLabel?: string; + amountLabel?: string; + amountClassifier?: 'current hour' | 'rolling 24h' | 'last 7 days'; + topDrivers?: SpendDriver[]; +}; + +export type CostInsightsSettingsData = { + owner: CostInsightsOwner; + enabled: boolean; + suggestionsEnabled: boolean; + thresholdUsd: string; + saveState: 'saved' | 'dirty' | 'saving' | 'error'; + validations?: string[]; + readOnly?: boolean; +}; + +export type SettingsConfirmation = + | 'enable_with_current_alerts' + | 'lower_threshold' + | 'disable_alerts'; + +export type ActivityFilter = 'all' | 'alerts' | 'suggestions' | 'reviews' | 'settings'; From 182f82a4353e9d641a05d89b2cc2f9e7bf3a7ddb Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Thu, 25 Jun 2026 19:22:35 +0200 Subject: [PATCH 02/11] feat(cost-insights): add page routes --- .../app/(app)/cost-insights/activity/page.tsx | 5 ++ .../app/(app)/cost-insights/ask-kilo/page.tsx | 5 ++ .../src/app/(app)/cost-insights/layout.tsx | 10 +++ apps/web/src/app/(app)/cost-insights/page.tsx | 5 ++ .../app/(app)/cost-insights/settings/page.tsx | 5 ++ .../[id]/cost-insights/activity/page.tsx | 5 ++ .../[id]/cost-insights/ask-kilo/page.tsx | 5 ++ .../[id]/cost-insights/layout.tsx | 27 +++++++ .../organizations/[id]/cost-insights/page.tsx | 5 ++ .../[id]/cost-insights/settings/page.tsx | 5 ++ .../cost-insights/CostInsightsLayout.tsx | 77 +++++++++++++++++++ .../CostInsightsRoutePlaceholder.tsx | 18 +++++ .../cost-insights/overview/AskKiloInput.tsx | 4 +- .../shell/CostInsightsShellView.tsx | 6 +- 14 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/(app)/cost-insights/activity/page.tsx create mode 100644 apps/web/src/app/(app)/cost-insights/ask-kilo/page.tsx create mode 100644 apps/web/src/app/(app)/cost-insights/layout.tsx create mode 100644 apps/web/src/app/(app)/cost-insights/page.tsx create mode 100644 apps/web/src/app/(app)/cost-insights/settings/page.tsx create mode 100644 apps/web/src/app/(app)/organizations/[id]/cost-insights/activity/page.tsx create mode 100644 apps/web/src/app/(app)/organizations/[id]/cost-insights/ask-kilo/page.tsx create mode 100644 apps/web/src/app/(app)/organizations/[id]/cost-insights/layout.tsx create mode 100644 apps/web/src/app/(app)/organizations/[id]/cost-insights/page.tsx create mode 100644 apps/web/src/app/(app)/organizations/[id]/cost-insights/settings/page.tsx create mode 100644 apps/web/src/components/cost-insights/CostInsightsLayout.tsx create mode 100644 apps/web/src/components/cost-insights/CostInsightsRoutePlaceholder.tsx diff --git a/apps/web/src/app/(app)/cost-insights/activity/page.tsx b/apps/web/src/app/(app)/cost-insights/activity/page.tsx new file mode 100644 index 0000000000..c00bdd8ee4 --- /dev/null +++ b/apps/web/src/app/(app)/cost-insights/activity/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; + +export default function CostInsightsActivityPage() { + return ; +} diff --git a/apps/web/src/app/(app)/cost-insights/ask-kilo/page.tsx b/apps/web/src/app/(app)/cost-insights/ask-kilo/page.tsx new file mode 100644 index 0000000000..d8481c6ed2 --- /dev/null +++ b/apps/web/src/app/(app)/cost-insights/ask-kilo/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; + +export default function CostInsightsAskKiloPage() { + return ; +} diff --git a/apps/web/src/app/(app)/cost-insights/layout.tsx b/apps/web/src/app/(app)/cost-insights/layout.tsx new file mode 100644 index 0000000000..ae2d4a68f9 --- /dev/null +++ b/apps/web/src/app/(app)/cost-insights/layout.tsx @@ -0,0 +1,10 @@ +import { CostInsightsLayout } from '@/components/cost-insights/CostInsightsLayout'; + +export const metadata = { + title: 'Cost Insights | Kilo Code', + description: 'Review Credit spend and configure Spend Alerts', +}; + +export default function CostInsightsRootLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/apps/web/src/app/(app)/cost-insights/page.tsx b/apps/web/src/app/(app)/cost-insights/page.tsx new file mode 100644 index 0000000000..5c0b567c4f --- /dev/null +++ b/apps/web/src/app/(app)/cost-insights/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; + +export default function CostInsightsPage() { + return ; +} diff --git a/apps/web/src/app/(app)/cost-insights/settings/page.tsx b/apps/web/src/app/(app)/cost-insights/settings/page.tsx new file mode 100644 index 0000000000..80341752ec --- /dev/null +++ b/apps/web/src/app/(app)/cost-insights/settings/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; + +export default function CostInsightsSettingsPage() { + return ; +} diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/activity/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/activity/page.tsx new file mode 100644 index 0000000000..03c32071d9 --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/activity/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; + +export default function OrganizationCostInsightsActivityPage() { + return ; +} diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/ask-kilo/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/ask-kilo/page.tsx new file mode 100644 index 0000000000..c907d94d10 --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/ask-kilo/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; + +export default function OrganizationCostInsightsAskKiloPage() { + return ; +} diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/layout.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/layout.tsx new file mode 100644 index 0000000000..57ed4586df --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/layout.tsx @@ -0,0 +1,27 @@ +import { CostInsightsLayout } from '@/components/cost-insights/CostInsightsLayout'; +import { OrganizationByPageLayout } from '@/components/organizations/OrganizationByPageLayout'; + +export const metadata = { + title: 'Cost Insights | Kilo Code', + description: 'Review organization Credit spend and configure Spend Alerts', +}; + +type LayoutProps = { + params: Promise<{ id: string }>; + children: React.ReactNode; +}; + +export default function OrganizationCostInsightsLayout({ params, children }: LayoutProps) { + return ( + ( + + {children} + + )} + /> + ); +} diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/page.tsx new file mode 100644 index 0000000000..811e0008d4 --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; + +export default function OrganizationCostInsightsPage() { + return ; +} diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/settings/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/settings/page.tsx new file mode 100644 index 0000000000..3c3347466f --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/settings/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; + +export default function OrganizationCostInsightsSettingsPage() { + return ; +} diff --git a/apps/web/src/components/cost-insights/CostInsightsLayout.tsx b/apps/web/src/components/cost-insights/CostInsightsLayout.tsx new file mode 100644 index 0000000000..bb48dda476 --- /dev/null +++ b/apps/web/src/components/cost-insights/CostInsightsLayout.tsx @@ -0,0 +1,77 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { Activity, LayoutDashboard, MessageCircle, Settings2 } from 'lucide-react'; +import { HideAppTopbar } from '@/components/gastown/HideAppTopbar'; +import { SidebarTrigger } from '@/components/ui/sidebar'; +import { cn } from '@/lib/utils'; + +type CostInsightsLayoutProps = { + basePath: string; + children: React.ReactNode; +}; + +const navItems = [ + { label: 'Overview', path: '', icon: LayoutDashboard }, + { label: 'Ask Kilo', path: '/ask-kilo', icon: MessageCircle }, + { label: 'Activity', path: '/activity', icon: Activity }, + { label: 'Alert settings', path: '/settings', icon: Settings2 }, +]; + +export function CostInsightsLayout({ basePath, children }: CostInsightsLayoutProps) { + const pathname = usePathname(); + + return ( +
+ +
+
+
+ +
+
+

Cost Insights

+

+ See what drives Credit spend and get notified when spending changes. +

+
+
+
+ + + +
+ {children} +
+
+ ); +} diff --git a/apps/web/src/components/cost-insights/CostInsightsRoutePlaceholder.tsx b/apps/web/src/components/cost-insights/CostInsightsRoutePlaceholder.tsx new file mode 100644 index 0000000000..a272e38367 --- /dev/null +++ b/apps/web/src/components/cost-insights/CostInsightsRoutePlaceholder.tsx @@ -0,0 +1,18 @@ +import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +export function CostInsightsRoutePlaceholder({ + section, +}: { + section: 'Overview' | 'Ask Kilo' | 'Activity' | 'Alert settings'; +}) { + return ( + + + {section} + + Cost Insights data will appear here when spend data is connected. + + + + ); +} diff --git a/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx b/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx index ea34e3fbc7..dee719103d 100644 --- a/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx +++ b/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx @@ -17,8 +17,8 @@ export function AskKiloInput({ const [question, setQuestion] = useState(''); const askHref = owner.type === 'organization' - ? '/organizations/acme-cost-insights/cost-insights?tab=ask' - : '/cost-insights?tab=ask'; + ? '/organizations/acme-cost-insights/cost-insights/ask-kilo' + : '/cost-insights/ask-kilo'; function handleSubmit(event: FormEvent) { event.preventDefault(); diff --git a/apps/web/src/components/cost-insights/shell/CostInsightsShellView.tsx b/apps/web/src/components/cost-insights/shell/CostInsightsShellView.tsx index 422e51ceda..87c279d0e9 100644 --- a/apps/web/src/components/cost-insights/shell/CostInsightsShellView.tsx +++ b/apps/web/src/components/cost-insights/shell/CostInsightsShellView.tsx @@ -30,9 +30,9 @@ export function CostInsightsShellView({ : '/cost-insights'; const navItems = [ { page: 'dashboard' as const, label: 'Overview', href: basePath }, - { page: 'ask' as const, label: 'Ask Kilo', href: `${basePath}?tab=ask` }, - { page: 'events' as const, label: 'Activity', href: `${basePath}?tab=events` }, - { page: 'settings' as const, label: 'Alert settings', href: `${basePath}/config` }, + { page: 'ask' as const, label: 'Ask Kilo', href: `${basePath}/ask-kilo` }, + { page: 'events' as const, label: 'Activity', href: `${basePath}/activity` }, + { page: 'settings' as const, label: 'Alert settings', href: `${basePath}/settings` }, ]; const roleLabel = owner.authorizedRole === 'billing_manager' From a1cdcd9f1c5b8363073662da8d783ad34a962299 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Thu, 25 Jun 2026 19:24:53 +0200 Subject: [PATCH 03/11] docs(cost-insights): clarify suggestion examples --- .../cost-insights/costInsightsFixtures.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/storybook/stories/cost-insights/costInsightsFixtures.ts b/apps/storybook/stories/cost-insights/costInsightsFixtures.ts index 6cb1739000..1355d393c6 100644 --- a/apps/storybook/stories/cost-insights/costInsightsFixtures.ts +++ b/apps/storybook/stories/cost-insights/costInsightsFixtures.ts @@ -270,11 +270,11 @@ export const kiloPassSuggestion = { eyebrow: 'Cost suggestion', title: 'Get more credits from your monthly spend with Kilo Pass Expert', description: - 'You spent $106.90 on pay-as-you-go credits in the last 7 days. Kilo Pass Expert converts your $199 monthly payment into paid credits and lets you earn up to $79.60 more in free bonus credits.', + 'You spent $106.90 on pay-as-you-go credits in the last 7 days, about $458 over 30 days at the same pace. Kilo Pass Expert costs $199 per month and includes $199 in paid credits, plus up to $79.60 in free bonus credits. Based on your recent spend, the plan could give you more credits for part of the spend you already make.', facts: [ - { label: 'Monthly payment', value: '$199' }, - { label: 'Paid credits', value: '$199' }, - { label: 'Potential bonus', value: '+$79.60' }, + { label: 'Last 7 days', value: '$106.90' }, + { label: '30-day pace', value: '~$458' }, + { label: 'Expert plan', value: '$199 + up to $79.60 bonus' }, ], ctaLabel: 'View Kilo Pass Expert', ctaHref: '/kilo-pass', @@ -286,11 +286,11 @@ export const codingPlanSuggestion = { eyebrow: 'Cost suggestion', title: 'Get more MiniMax usage with Token Plan Plus', description: - 'You spent $15.00 on MiniMax in the last 7 days. For $20 every 30 days, Token Plan Plus includes about 1.7B M3 tokens and access to the full MiniMax model family.', + 'You spent $15.00 on MiniMax in the last 7 days, about $64 over 30 days at the same pace. Token Plan Plus costs $20 every 30 days and includes about 1.7B M3 tokens with access to the full MiniMax model family.', facts: [ - { label: 'Plan price', value: '$20' }, - { label: 'Included usage', value: '~1.7B tokens' }, - { label: 'Renews every', value: '30 days' }, + { label: 'Last 7 days', value: '$15.00' }, + { label: '30-day pace', value: '~$64' }, + { label: 'Plan price', value: '$20 every 30 days' }, ], ctaLabel: 'View MiniMax plan', ctaHref: '/coding-plans/minimax', From cb0f92a5db31e5c839a052da9640f9f3610cb52a Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Thu, 25 Jun 2026 19:35:59 +0200 Subject: [PATCH 04/11] feat(cost-insights): add spend rollup data layer --- apps/web/src/app/api/exa/[...path]/route.ts | 7 +- .../src/lib/ai-gateway/processUsage.test.ts | 103 +- apps/web/src/lib/ai-gateway/processUsage.ts | 146 ++- .../billing-lifecycle-cron.test.ts | 71 +- .../coding-plans/billing-lifecycle-cron.ts | 21 + apps/web/src/lib/coding-plans/index.test.ts | 63 + apps/web/src/lib/coding-plans/index.ts | 17 + .../cost-insights/canonical-sources.test.ts | 253 ++++ .../lib/cost-insights/canonical-sources.ts | 943 ++++++++++++++ .../cost-insights-rollups-script.test.ts | 93 ++ .../rollup-maintenance.integration.test.ts | 320 +++++ .../cost-insights/rollup-maintenance.test.ts | 156 +++ .../lib/cost-insights/rollup-maintenance.ts | 1122 +++++++++++++++++ .../spend-repository.integration.test.ts | 199 +++ .../cost-insights/spend-repository.test.ts | 160 +++ .../src/lib/cost-insights/spend-repository.ts | 696 ++++++++++ .../cost-insights/spend-writer-audit.test.ts | 57 + apps/web/src/lib/exa-paths.ts | 27 + .../lib/exa-usage-log-indexes-script.test.ts | 109 ++ apps/web/src/lib/exa-usage-partitions.test.ts | 84 ++ apps/web/src/lib/exa-usage-partitions.ts | 62 +- apps/web/src/lib/exa-usage.test.ts | 157 ++- apps/web/src/lib/exa-usage.ts | 207 +-- apps/web/src/lib/kiloclaw/credit-billing.ts | 19 + .../organizations/organization-usage.test.ts | 39 + .../lib/organizations/organization-usage.ts | 188 +-- .../routers/kiloclaw-billing-router.test.ts | 61 + .../src/scripts/db/cost-insights-rollups.ts | 183 +++ .../src/scripts/db/exa-usage-log-indexes.ts | 169 +++ dev/seed/cost-insights/spend-evidence.ts | 734 +++++++++++ packages/db/package.json | 1 + packages/db/src/cost-insights-rollups.test.ts | 585 +++++++++ packages/db/src/cost-insights-rollups.ts | 435 +++++++ packages/db/src/schema-types.ts | 29 + packages/db/src/schema.test.ts | 7 + packages/db/src/schema.ts | 251 ++++ .../kiloclaw-billing/src/lifecycle.test.ts | 50 + services/kiloclaw-billing/src/lifecycle.ts | 20 + 38 files changed, 7627 insertions(+), 217 deletions(-) create mode 100644 apps/web/src/lib/cost-insights/canonical-sources.test.ts create mode 100644 apps/web/src/lib/cost-insights/canonical-sources.ts create mode 100644 apps/web/src/lib/cost-insights/cost-insights-rollups-script.test.ts create mode 100644 apps/web/src/lib/cost-insights/rollup-maintenance.integration.test.ts create mode 100644 apps/web/src/lib/cost-insights/rollup-maintenance.test.ts create mode 100644 apps/web/src/lib/cost-insights/rollup-maintenance.ts create mode 100644 apps/web/src/lib/cost-insights/spend-repository.integration.test.ts create mode 100644 apps/web/src/lib/cost-insights/spend-repository.test.ts create mode 100644 apps/web/src/lib/cost-insights/spend-repository.ts create mode 100644 apps/web/src/lib/cost-insights/spend-writer-audit.test.ts create mode 100644 apps/web/src/lib/exa-paths.ts create mode 100644 apps/web/src/lib/exa-usage-log-indexes-script.test.ts create mode 100644 apps/web/src/lib/exa-usage-partitions.test.ts create mode 100644 apps/web/src/scripts/db/cost-insights-rollups.ts create mode 100644 apps/web/src/scripts/db/exa-usage-log-indexes.ts create mode 100644 dev/seed/cost-insights/spend-evidence.ts create mode 100644 packages/db/src/cost-insights-rollups.test.ts create mode 100644 packages/db/src/cost-insights-rollups.ts diff --git a/apps/web/src/app/api/exa/[...path]/route.ts b/apps/web/src/app/api/exa/[...path]/route.ts index daf6b60960..3c88bcbbf2 100644 --- a/apps/web/src/app/api/exa/[...path]/route.ts +++ b/apps/web/src/app/api/exa/[...path]/route.ts @@ -13,16 +13,15 @@ import { getBalanceAndOrgSettings } from '@/lib/organizations/organization-usage import { readDb } from '@/lib/drizzle'; import { captureException } from '@sentry/nextjs'; import { validateFeatureHeader, FEATURE_HEADER } from '@/lib/feature-detection'; +import { EXA_ALLOWED_PATHS, isExaAllowedPath } from '@/lib/exa-paths'; const EXA_BASE_URL = 'https://api.exa.ai'; -const ALLOWED_PATHS = new Set(['/search', '/contents', '/findSimilar', '/answer', '/context']); - function extractExaPath(url: URL): string | null { const prefix = '/api/exa'; if (!url.pathname.startsWith(prefix)) return null; const path = url.pathname.slice(prefix.length); - return ALLOWED_PATHS.has(path) ? path : null; + return isExaAllowedPath(path) ? path : null; } function extractCostDollars(responseBody: unknown): number | undefined { @@ -40,7 +39,7 @@ export async function POST(request: NextRequest) { const exaPath = extractExaPath(url); if (!exaPath) { return NextResponse.json( - { error: `Invalid path. Allowed: ${[...ALLOWED_PATHS].join(', ')}` }, + { error: `Invalid path. Allowed: ${EXA_ALLOWED_PATHS.join(', ')}` }, { status: 400 } ); } diff --git a/apps/web/src/lib/ai-gateway/processUsage.test.ts b/apps/web/src/lib/ai-gateway/processUsage.test.ts index e958c40e93..a57392e1a2 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.test.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.test.ts @@ -7,6 +7,7 @@ import { parseMicrodollarUsageFromString, mapToUsageStats, logMicrodollarUsage, + insertUsageRecord, processOpenRouterUsage, stripNulBytesInPlace, toInsertableDbUsageRecord, @@ -14,20 +15,28 @@ import { import type { OpenRouterGeneration } from '@/lib/ai-gateway/providers/openrouter/types'; import { verifyApproval } from '../../tests/helpers/approval.helper'; import { insertTestUser } from '../../tests/helpers/user.helper'; -import { insertUsageWithOverrides } from '../../tests/helpers/microdollar-usage.helper'; +import { + defineMicrodollarUsage, + insertUsageWithOverrides, +} from '../../tests/helpers/microdollar-usage.helper'; import { join } from 'node:path'; import { createReadStream } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { db } from '@/lib/drizzle'; import { + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, microdollar_usage, microdollar_usage_daily, microdollar_usage_metadata, + organization_user_usage, + organizations, } from '@kilocode/db/schema'; import { and, eq, getTableColumns, isNull, sql } from 'drizzle-orm'; import { findUserById } from '../user'; import { Readable } from 'node:stream'; import { getFraudDetectionHeaders, toMicrodollars } from '../utils'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; // Note: Legacy banned_ja4/whitelist_ja4 tests removed - abuse classification // is now handled by the external abuse detection service (src/lib/abuse-service.ts) @@ -418,6 +427,32 @@ describe('logMicrodollarUsage', () => { expect(usageRecord?.has_error).toBe(false); expect(usageRecord?.created_at).toBeTruthy(); expect(metadataRecord?.created_at).toBe(usageRecord?.created_at); + + const [total] = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, user.id)); + expect(total).toMatchObject({ + owned_by_organization_id: null, + spend_category: 'variable', + total_microdollars: 500, + spend_record_count: 1, + }); + + const [driver] = await db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, user.id)); + expect(driver).toMatchObject({ + source: 'ai_gateway', + product_key: 'vscode-extension', + feature_key: 'chat_completions', + model_or_plan_key: 'anthropic/claude-3.7-sonnet', + provider_key: 'Provider', + actor_user_id: user.id, + total_microdollars: 500, + spend_record_count: 1, + }); }); test('stores session_id when provided', async () => { @@ -525,6 +560,34 @@ describe('logMicrodollarUsage', () => { expect(usageRecord?.has_error).toBe(true); expect(usageRecord?.model).toBe('openai/gpt-4.1'); expect(metadataRecord?.has_middle_out_transform).toBe(false); + + const totals = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, user.id)); + expect(totals).toHaveLength(0); + }); + + test('rolls back AI source rows when mandatory capture fails', async () => { + const { core, metadata } = await defineMicrodollarUsage(); + const missingUserId = `missing-ai-user-${crypto.randomUUID()}`; + + const result = await insertUsageRecord( + { ...core, kilo_user_id: missingUserId, cost: 1000 }, + metadata + ); + + expect(result).toBeNull(); + const sourceRows = await db + .select() + .from(microdollar_usage) + .where(eq(microdollar_usage.kilo_user_id, missingUserId)); + const totals = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, missingUserId)); + expect(sourceRows).toHaveLength(0); + expect(totals).toHaveLength(0); }); test('stores 3 usage records with overlapping data and tests metadata deduplication', async () => { @@ -661,6 +724,7 @@ describe('logMicrodollarUsage', () => { google_user_email: 'orguser@example.com', }); + const organization = await createTestOrganization('AI usage organization', user.id, 10_000); const usageStats: MicrodollarUsageStats = { ...BASE_USAGE_STATS, messageId: 'test-org-msg-123', @@ -668,7 +732,7 @@ describe('logMicrodollarUsage', () => { const usageContext: MicrodollarUsageContext = { ...createBaseUsageContext(user), - organizationId: '12345678-1234-1234-1234-123456789abc', // This triggers data minimization + organizationId: organization.id, }; await logMicrodollarUsage(usageStats, usageContext); @@ -685,7 +749,7 @@ describe('logMicrodollarUsage', () => { expect(usageRecord).toBeTruthy(); expect(usageRecord?.kilo_user_id).toBe('test-org-user-1'); - expect(usageRecord?.organization_id).toBe('12345678-1234-1234-1234-123456789abc'); + expect(usageRecord?.organization_id).toBe(organization.id); expect(usageRecord?.cost).toBe(500); // Verify data minimization: sensitive prompt data should be null for organizations @@ -697,6 +761,32 @@ describe('logMicrodollarUsage', () => { expect(usageRecord?.output_tokens).toBe(50); expect(usageRecord?.model).toBe('anthropic/claude-3.7-sonnet'); expect(usageRecord?.provider).toBe('openrouter'); + + const [chargedOrganization] = await db + .select({ microdollars_used: organizations.microdollars_used }) + .from(organizations) + .where(eq(organizations.id, organization.id)); + expect(chargedOrganization.microdollars_used).toBe(500); + + const [memberUsage] = await db + .select() + .from(organization_user_usage) + .where(eq(organization_user_usage.organization_id, organization.id)); + expect(memberUsage).toMatchObject({ + kilo_user_id: user.id, + microdollar_usage: 500, + }); + + const [total] = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_organization_id, organization.id)); + expect(total).toMatchObject({ + owned_by_user_id: null, + spend_category: 'variable', + total_microdollars: 500, + spend_record_count: 1, + }); }); test('insertUsageRecord updates user balance atomically via insertUsageWithOverrides', async () => { @@ -752,10 +842,12 @@ describe('logMicrodollarUsage', () => { google_user_email: 'insertorg@example.com', }); + const organization = await createTestOrganization('Insert usage organization', user.id, 10_000); + // Insert usage record with organization_id (should not update user balance) await insertUsageWithOverrides({ kilo_user_id: user.id, - organization_id: '12345678-1234-1234-1234-123456789abc', + organization_id: organization.id, cost: 2000, }); @@ -823,7 +915,8 @@ describe('logMicrodollarUsage', () => { microdollars_used: 0, google_user_email: 'daily-org-scope@example.com', }); - const orgId = '11111111-1111-1111-1111-111111111111'; + const organization = await createTestOrganization('Daily usage organization', user.id, 10_000); + const orgId = organization.id; await insertUsageWithOverrides({ kilo_user_id: user.id, cost: 500 }); await insertUsageWithOverrides({ diff --git a/apps/web/src/lib/ai-gateway/processUsage.ts b/apps/web/src/lib/ai-gateway/processUsage.ts index 2b8ef10d75..af45b6b7ed 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.ts @@ -19,7 +19,12 @@ import { hasPaymentMethod } from '@/lib/admin-utils-serverside'; import type { SQL } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm'; import { sentryRootSpan } from '../getRootSpan'; -import { ingestOrganizationTokenUsage } from '@/lib/organizations/organization-usage'; +import { + mutateOrganizationUsage, + scheduleOrganizationLowBalanceAlert, +} from '@/lib/organizations/organization-usage'; +import type { OrganizationUsageMutationResult } from '@/lib/organizations/organization-usage'; +import type { DrizzleTransaction } from '@/lib/drizzle'; import type { ProviderId } from '@/lib/ai-gateway/providers/types'; import { findKiloExclusiveModel, @@ -69,6 +74,11 @@ import { type KiloExclusiveModel, } from '@/lib/ai-gateway/providers/kilo-exclusive-model'; import { calculateCustomCost_mUsd } from '@/lib/ai-gateway/custom-pricing'; +import { captureCostInsightSpend } from '@kilocode/db/cost-insights-rollups'; +import { + getAiGatewayCostInsightFeatureKey, + getAiGatewayCostInsightProductKey, +} from '@/lib/cost-insights/canonical-sources'; const posthogClient = PostHogClient(); @@ -305,7 +315,6 @@ async function saveUsageRelatedData( isFirst ); } - await ingestOrganizationTokenUsage(coreUsageFields); return inserted; } @@ -410,6 +419,82 @@ ${metaDataKindName}_cte AS ( SELECT ${metaDataKindName}_id FROM ${metaDataKindName}_ins )`; +type UsageStatementExecutor = Pick; + +type UsageStatementResult = UsageRecordInsertResult & { + kiloPassThreshold: number | null; +}; + +type UsageTransactionResult = { + inserted: UsageStatementResult; + organizationUsage: OrganizationUsageMutationResult | null; +}; + +async function insertUsageTransaction( + coreUsageFields: MicrodollarUsage, + metadataFields: UsageMetaData +): Promise { + return db.transaction(async tx => { + const inserted = await insertUsageAndMetadataWithBalanceUpdate( + tx, + coreUsageFields, + metadataFields + ); + const organizationUsage = coreUsageFields.organization_id + ? await mutateOrganizationUsage(tx, coreUsageFields) + : null; + + if (coreUsageFields.cost > 0) { + await captureCostInsightSpend(tx, { + owner: coreUsageFields.organization_id + ? { type: 'organization', id: coreUsageFields.organization_id } + : { type: 'user', id: coreUsageFields.kilo_user_id }, + actorUserId: coreUsageFields.kilo_user_id, + occurredAt: coreUsageFields.created_at, + amountMicrodollars: coreUsageFields.cost, + category: 'variable', + source: 'ai_gateway', + productKey: getAiGatewayCostInsightProductKey(metadataFields.feature), + featureKey: getAiGatewayCostInsightFeatureKey(metadataFields.api_kind), + modelOrPlanKey: coreUsageFields.requested_model || coreUsageFields.model || 'other', + providerKey: coreUsageFields.inference_provider || coreUsageFields.provider || 'other', + }); + } + + return { inserted, organizationUsage }; + }); +} + +function scheduleKiloPassBonusIfNeeded( + usage: MicrodollarUsage, + result: UsageStatementResult +): void { + if (result.newMicrodollarsUsed === null) return; + const effectiveKiloPassThreshold = getEffectiveKiloPassThreshold(result.kiloPassThreshold); + if ( + effectiveKiloPassThreshold === null || + result.newMicrodollarsUsed < effectiveKiloPassThreshold + ) { + return; + } + + void maybeIssueKiloPassBonusFromUsageThreshold({ + kiloUserId: usage.kilo_user_id, + nowIso: usage.created_at, + }).catch(async error => { + const errorMessage = error instanceof Error ? error.message : String(error); + await appendKiloPassAuditLog(db, { + action: KiloPassAuditLogAction.BonusCreditsIssued, + result: KiloPassAuditLogResult.Failed, + kiloUserId: usage.kilo_user_id, + payload: { + source: 'usage_threshold', + error: errorMessage, + }, + }); + }); +} + export async function insertUsageRecord( coreUsageFields: MicrodollarUsage, metadataFields: UsageMetaData @@ -424,8 +509,9 @@ export async function insertUsageRecord( let attempt = 0; while (true) { try { - //this can fail if new deduplicated values are inserted simultaneously - return await insertUsageAndMetadataWithBalanceUpdate(coreUsageFields, metadataFields); + // This can fail if new deduplicated values are inserted simultaneously. + // Every retry opens a fresh transaction for source, charge, and rollups. + return await insertUsageTransaction(coreUsageFields, metadataFields); } catch (error) { if (attempt >= 2) throw error; sentryLogger('insertUsageRecord', 'warning')( @@ -438,21 +524,39 @@ export async function insertUsageRecord( } } ); - return result; + + if (coreUsageFields.organization_id && result.organizationUsage) { + scheduleOrganizationLowBalanceAlert( + coreUsageFields.organization_id, + result.organizationUsage + ); + } + scheduleKiloPassBonusIfNeeded(coreUsageFields, result.inserted); + return { + usageId: result.inserted.usageId, + createdAt: result.inserted.createdAt, + newMicrodollarsUsed: result.inserted.newMicrodollarsUsed, + }; } catch (error) { console.error('insertUsageRecord failed', error); captureException(error, { - tags: { source: 'insertUsageRecord' }, - extra: { coreUsageFields, metadataFields }, + tags: { + source: 'insertUsageRecord', + spendCategory: 'variable', + spendSource: 'ai_gateway', + ownerType: coreUsageFields.organization_id ? 'organization' : 'user', + }, + extra: { sourceRecordId: coreUsageFields.id }, }); return null; } } async function insertUsageAndMetadataWithBalanceUpdate( + executor: UsageStatementExecutor, coreUsageFields: MicrodollarUsage, metadataFields: UsageMetaData -): Promise { +): Promise { // Pick the matching partial unique index for the daily-rollup upsert. The // microdollar_usage_daily table has two partial unique indexes; the upsert // must target the one corresponding to this row's scope. @@ -463,7 +567,7 @@ async function insertUsageAndMetadataWithBalanceUpdate( // Use a single SQL statement with CTEs to insert usage, upsert all lookup values, metadata, and update user balance in one roundtrip // This ensures atomicity: microdollar_usage insert and kilocode_users.microdollars_used update happen together - const result = await db.execute<{ + const result = await executor.execute<{ usage_id: string; usage_created_at: string; new_microdollars_used: number | null; @@ -647,33 +751,11 @@ async function insertUsageAndMetadataWithBalanceUpdate( const kiloPassThreshold = inserted.kilo_pass_threshold == null ? null : Number(inserted.kilo_pass_threshold); - if (newMicrodollarsUsed !== null) { - const effectiveKiloPassThreshold = getEffectiveKiloPassThreshold(kiloPassThreshold); - - if (effectiveKiloPassThreshold !== null && newMicrodollarsUsed >= effectiveKiloPassThreshold) { - // Trigger this async to avoid blocking - void maybeIssueKiloPassBonusFromUsageThreshold({ - kiloUserId: coreUsageFields.kilo_user_id, - nowIso: coreUsageFields.created_at, - }).catch(async error => { - const errorMessage = error instanceof Error ? error.message : String(error); - await appendKiloPassAuditLog(db, { - action: KiloPassAuditLogAction.BonusCreditsIssued, - result: KiloPassAuditLogResult.Failed, - kiloUserId: coreUsageFields.kilo_user_id, - payload: { - source: 'usage_threshold', - error: errorMessage, - }, - }); - }); - } - } - return { usageId: inserted.usage_id, createdAt: inserted.usage_created_at, newMicrodollarsUsed, + kiloPassThreshold, }; } diff --git a/apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts b/apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts index fa2ae285f2..2ba4d411be 100644 --- a/apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts +++ b/apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts @@ -1,5 +1,6 @@ /* eslint-disable drizzle/enforce-delete-with-where */ import { eq } from 'drizzle-orm'; +import type { CaptureCostInsightSpendInput } from '@kilocode/db/cost-insights-rollups'; import { runCodingPlanBillingLifecycleCron } from '@/lib/coding-plans/billing-lifecycle-cron'; import { subscribeToCodingPlan, uploadKeysToInventory } from '@/lib/coding-plans'; @@ -11,6 +12,8 @@ import { coding_plan_key_inventory, coding_plan_subscriptions, coding_plan_terms, + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, credit_transactions, kilocode_users, } from '@kilocode/db/schema'; @@ -18,6 +21,14 @@ import { jest.mock('@/lib/autoTopUp', () => ({ maybePerformAutoTopUp: jest.fn(async () => undefined), })); +jest.mock('@kilocode/db/cost-insights-rollups', () => ({ + captureCostInsightSpend: jest.fn(async () => undefined), + COST_INSIGHT_CODING_PLAN_PRODUCT_KEY: 'coding-plan', +})); + +const captureCostInsightSpendMock = jest.requireMock<{ + captureCostInsightSpend: jest.Mock, [unknown, CaptureCostInsightSpendInput]>; +}>('@kilocode/db/cost-insights-rollups').captureCostInsightSpend; const PLAN_ID = 'minimax-token-plan-plus'; const COST_MICRODOLLARS = 20_000_000; @@ -46,6 +57,9 @@ async function createSubscription(balance = COST_MICRODOLLARS, autoTopUpEnabled afterEach(async () => { jest.mocked(maybePerformAutoTopUp).mockClear(); + captureCostInsightSpendMock.mockClear(); + await db.delete(cost_insight_owner_hour_driver_buckets); + await db.delete(cost_insight_owner_hour_totals); await db.delete(coding_plan_terms); await db.delete(coding_plan_subscriptions); await db.delete(byok_api_keys); @@ -56,7 +70,8 @@ afterEach(async () => { describe('Coding Plan billing lifecycle cron', () => { it('renews atomically with a charged term and retains assigned access', async () => { - const { subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2); + const { user, subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2); + captureCostInsightSpendMock.mockClear(); const summary = await runCodingPlanBillingLifecycleCron(db); const [subscription] = await db @@ -72,7 +87,10 @@ describe('Coding Plan billing lifecycle cron', () => { .from(coding_plan_key_inventory) .where(eq(coding_plan_key_inventory.id, subscription.key_inventory_id!)); const renewalTransaction = await db - .select({ description: credit_transactions.description }) + .select({ + description: credit_transactions.description, + createdAt: credit_transactions.created_at, + }) .from(credit_transactions) .where(eq(credit_transactions.description, 'Coding plan renewal: MiniMax Token Plan Plus')); @@ -80,11 +98,58 @@ describe('Coding Plan billing lifecycle cron', () => { expect(subscription.status).toBe('active'); expect(terms.map(term => term.kind)).toEqual(['activation', 'renewal']); expect(renewalTransaction).toEqual([ - { description: 'Coding plan renewal: MiniMax Token Plan Plus' }, + { + description: 'Coding plan renewal: MiniMax Token Plan Plus', + createdAt: expect.any(String), + }, ]); + expect(captureCostInsightSpendMock).toHaveBeenCalledWith(expect.anything(), { + owner: { type: 'user', id: user.id }, + actorUserId: user.id, + occurredAt: expect.any(String), + amountMicrodollars: COST_MICRODOLLARS, + category: 'scheduled', + source: 'coding_plan', + productKey: 'coding-plan', + featureKey: 'renewal', + modelOrPlanKey: PLAN_ID, + providerKey: 'minimax', + }); + const captureInput = captureCostInsightSpendMock.mock.calls[0]?.[1] as + | { occurredAt: string } + | undefined; + expect(new Date(renewalTransaction[0].createdAt).toISOString()).toBe(captureInput?.occurredAt); expect(credential.status).toBe('assigned'); }); + it('rolls back renewal when scheduled-spend capture fails', async () => { + const { user, subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2); + captureCostInsightSpendMock.mockClear(); + captureCostInsightSpendMock.mockImplementationOnce(async () => { + throw new Error('rollup unavailable'); + }); + + const summary = await runCodingPlanBillingLifecycleCron(db); + const [updatedUser] = await db + .select({ used: kilocode_users.microdollars_used }) + .from(kilocode_users) + .where(eq(kilocode_users.id, user.id)); + const terms = await db + .select() + .from(coding_plan_terms) + .where(eq(coding_plan_terms.subscription_id, subscriptionId)); + const renewalTransactions = await db + .select() + .from(credit_transactions) + .where(eq(credit_transactions.description, 'Coding plan renewal: MiniMax Token Plan Plus')); + + expect(summary.errors).toBe(1); + expect(summary.renewals).toBe(0); + expect(updatedUser.used).toBe(COST_MICRODOLLARS); + expect(terms.map(term => term.kind)).toEqual(['activation']); + expect(renewalTransactions).toHaveLength(0); + }); + it('renews after the subscriber deletes the installed MiniMax BYOK key', async () => { const { subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2); const [before] = await db diff --git a/apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts b/apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts index 21e691ad61..6842addc82 100644 --- a/apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts +++ b/apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts @@ -8,6 +8,10 @@ import { maybePerformAutoTopUp } from '@/lib/autoTopUp'; import { getCodingPlanPrice } from '@/lib/coding-plans/pricing'; import { maybeIssueKiloPassBonusFromUsageThreshold } from '@/lib/kilo-pass/usage-triggered-bonus'; import { sentryLogger } from '@/lib/utils.server'; +import { + captureCostInsightSpend, + COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, +} from '@kilocode/db/cost-insights-rollups'; import { byok_api_keys, coding_plan_key_inventory, @@ -38,6 +42,7 @@ type RenewalRow = { installed_byok_key_id: string | null; key_inventory_id: string | null; plan_id: string; + provider_id: string; status: 'active' | 'past_due'; cost_microdollars: number; billing_period_days: number; @@ -178,6 +183,7 @@ async function sweepRenewals( installed_byok_key_id: coding_plan_subscriptions.installed_byok_key_id, key_inventory_id: coding_plan_subscriptions.key_inventory_id, plan_id: coding_plan_subscriptions.plan_id, + provider_id: coding_plan_subscriptions.provider_id, status: coding_plan_subscriptions.status, cost_microdollars: coding_plan_subscriptions.cost_microdollars, billing_period_days: coding_plan_subscriptions.billing_period_days, @@ -282,6 +288,7 @@ async function processRenewal( installed_byok_key_id: coding_plan_subscriptions.installed_byok_key_id, key_inventory_id: coding_plan_subscriptions.key_inventory_id, plan_id: coding_plan_subscriptions.plan_id, + provider_id: coding_plan_subscriptions.provider_id, status: coding_plan_subscriptions.status, cost_microdollars: coding_plan_subscriptions.cost_microdollars, billing_period_days: coding_plan_subscriptions.billing_period_days, @@ -328,6 +335,7 @@ async function processRenewal( row.billing_period_days ).toISOString(); const transactionId = crypto.randomUUID(); + const occurredAt = new Date().toISOString(); const plan = getCodingPlanPrice(row.plan_id); const renewalDescription = plan ? `Coding plan renewal: ${plan.providerName} ${plan.name}` @@ -341,6 +349,19 @@ async function processRenewal( credit_category: `coding-plan:${renewalKey}`, check_category_uniqueness: true, original_baseline_microdollars_used: row.microdollars_used, + created_at: occurredAt, + }); + await captureCostInsightSpend(tx, { + owner: { type: 'user', id: row.user_id }, + actorUserId: row.user_id, + occurredAt, + amountMicrodollars: row.cost_microdollars, + category: 'scheduled', + source: 'coding_plan', + productKey: COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, + featureKey: 'renewal', + modelOrPlanKey: row.plan_id, + providerKey: row.provider_id, }); await tx.insert(coding_plan_terms).values({ subscription_id: row.id, diff --git a/apps/web/src/lib/coding-plans/index.test.ts b/apps/web/src/lib/coding-plans/index.test.ts index be2b81d787..7ad3d619c2 100644 --- a/apps/web/src/lib/coding-plans/index.test.ts +++ b/apps/web/src/lib/coding-plans/index.test.ts @@ -1,5 +1,6 @@ /* eslint-disable drizzle/enforce-delete-with-where */ import { eq } from 'drizzle-orm'; +import type { CaptureCostInsightSpendInput } from '@kilocode/db/cost-insights-rollups'; import { encryptApiKey } from '@/lib/ai-gateway/byok/encryption'; import { BYOK_ENCRYPTION_KEY } from '@/lib/config.server'; @@ -23,10 +24,21 @@ import { coding_plan_key_inventory, coding_plan_subscriptions, coding_plan_terms, + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, credit_transactions, kilocode_users, } from '@kilocode/db/schema'; +jest.mock('@kilocode/db/cost-insights-rollups', () => ({ + captureCostInsightSpend: jest.fn(async () => undefined), + COST_INSIGHT_CODING_PLAN_PRODUCT_KEY: 'coding-plan', +})); + +const captureCostInsightSpendMock = jest.requireMock<{ + captureCostInsightSpend: jest.Mock, [unknown, CaptureCostInsightSpendInput]>; +}>('@kilocode/db/cost-insights-rollups').captureCostInsightSpend; + const PLAN_ID = 'minimax-token-plan-plus'; const PROVIDER_ID = 'minimax'; const COST_MICRODOLLARS = 20_000_000; @@ -49,6 +61,9 @@ async function createUserWithBalance(microdollars: number) { } afterEach(async () => { + captureCostInsightSpendMock.mockClear(); + await db.delete(cost_insight_owner_hour_driver_buckets); + await db.delete(cost_insight_owner_hour_totals); await db.delete(coding_plan_terms); await db.delete(coding_plan_subscriptions); await db.delete(byok_api_keys); @@ -82,6 +97,10 @@ describe('coding plans', () => { .select() .from(coding_plan_terms) .where(eq(coding_plan_terms.subscription_id, result.subscriptionId)); + const [deduction] = await db + .select() + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); const [managedKey] = await db .select() .from(byok_api_keys) @@ -97,6 +116,22 @@ describe('coding plans', () => { expect(terms).toHaveLength(1); expect(terms[0].kind).toBe('activation'); expect(terms[0].cost_microdollars).toBe(COST_MICRODOLLARS); + expect(captureCostInsightSpendMock).toHaveBeenCalledWith(expect.anything(), { + owner: { type: 'user', id: user.id }, + actorUserId: user.id, + occurredAt: expect.any(String), + amountMicrodollars: COST_MICRODOLLARS, + category: 'scheduled', + source: 'coding_plan', + productKey: 'coding-plan', + featureKey: 'activation', + modelOrPlanKey: PLAN_ID, + providerKey: PROVIDER_ID, + }); + const captureInput = captureCostInsightSpendMock.mock.calls[0]?.[1] as + | { occurredAt: string } + | undefined; + expect(new Date(deduction.created_at).toISOString()).toBe(captureInput?.occurredAt); expect(managedKey.provider_id).toBe(PROVIDER_ID); expect(managedKey.management_source).toBe('coding_plan'); expect(inventoryKey.status).toBe('assigned'); @@ -124,6 +159,7 @@ describe('coding plans', () => { expect(terms).toHaveLength(1); expect(assigned).toHaveLength(1); expect(updatedUser.microdollars_used).toBe(COST_MICRODOLLARS); + expect(captureCostInsightSpendMock).toHaveBeenCalledTimes(1); }); it('rejects a new purchase while an active subscription exists', async () => { @@ -205,6 +241,33 @@ describe('coding plans', () => { expect(unchargedUser.microdollars_used).toBe(0); }); + it('rolls back activation when scheduled-spend capture fails', async () => { + const user = await createUserWithBalance(COST_MICRODOLLARS); + await seedInventoryKey(); + captureCostInsightSpendMock.mockImplementationOnce(async () => { + throw new Error('rollup unavailable'); + }); + + await expect(subscribeToCodingPlan(user.id, PLAN_ID, 'rollup-failure')).rejects.toThrow( + 'rollup unavailable' + ); + + const [updatedUser] = await db + .select() + .from(kilocode_users) + .where(eq(kilocode_users.id, user.id)); + const transactions = await db.select().from(credit_transactions); + const terms = await db.select().from(coding_plan_terms); + const subscriptions = await db.select().from(coding_plan_subscriptions); + const [inventory] = await db.select().from(coding_plan_key_inventory); + + expect(updatedUser.microdollars_used).toBe(0); + expect(transactions).toHaveLength(0); + expect(terms).toHaveLength(0); + expect(subscriptions).toHaveLength(0); + expect(inventory.status).toBe('available'); + }); + it('rejects activation when the personal MiniMax BYOK slot is occupied', async () => { const user = await createUserWithBalance(COST_MICRODOLLARS); await seedInventoryKey(); diff --git a/apps/web/src/lib/coding-plans/index.ts b/apps/web/src/lib/coding-plans/index.ts index ad02ee7ad7..49091a1d8e 100644 --- a/apps/web/src/lib/coding-plans/index.ts +++ b/apps/web/src/lib/coding-plans/index.ts @@ -12,6 +12,10 @@ import { getCodingPlanPrice, type CodingPlanId } from '@/lib/coding-plans/pricin import { db } from '@/lib/drizzle'; import { maybeIssueKiloPassBonusFromUsageThreshold } from '@/lib/kilo-pass/usage-triggered-bonus'; import { sentryLogger } from '@/lib/utils.server'; +import { + captureCostInsightSpend, + COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, +} from '@kilocode/db/cost-insights-rollups'; import { byok_api_keys, coding_plan_availability_intents, @@ -169,6 +173,19 @@ export async function subscribeToCodingPlan( credit_category: `coding-plan:${plan.planId}:${requestKey}`, check_category_uniqueness: true, original_baseline_microdollars_used: lockedUser.microdollars_used, + created_at: periodStartIso, + }); + await captureCostInsightSpend(tx, { + owner: { type: 'user', id: userId }, + actorUserId: userId, + occurredAt: periodStartIso, + amountMicrodollars: plan.costMicrodollars, + category: 'scheduled', + source: 'coding_plan', + productKey: COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, + featureKey: 'activation', + modelOrPlanKey: plan.planId, + providerKey: plan.providerId, }); const { rows: inventoryRows } = await tx.execute<{ diff --git a/apps/web/src/lib/cost-insights/canonical-sources.test.ts b/apps/web/src/lib/cost-insights/canonical-sources.test.ts new file mode 100644 index 0000000000..b82e4bcaad --- /dev/null +++ b/apps/web/src/lib/cost-insights/canonical-sources.test.ts @@ -0,0 +1,253 @@ +import { + aggregateCanonicalCostInsightDrivers, + loadCanonicalCostInsightAggregationsByHour, + mapAiGatewayCanonicalDriver, + mapCodingPlanCanonicalDriver, + mapExaCanonicalDriver, + mapKiloClawCanonicalDriver, + parseSafeDatabaseInteger, + type CostInsightQueryExecutor, +} from './canonical-sources'; + +const userOwner = { type: 'user', id: 'user-1' } as const; + +describe('Cost Insights canonical source mapping', () => { + test('maps AI Gateway dimensions with requested model and inference provider precedence', () => { + const mapped = mapAiGatewayCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + feature: 'cloud-agent', + apiKind: 'responses', + requestedModel: 'requested/model', + resolvedModel: 'resolved/model', + inferenceProvider: 'inference-provider', + gatewayProvider: 'gateway-provider', + totalMicrodollars: 17, + spendRecordCount: 1, + }); + + expect(mapped.driver).toMatchObject({ + category: 'variable', + source: 'ai_gateway', + productKey: 'cloud-agent', + featureKey: 'responses', + modelOrPlanKey: 'requested/model', + providerKey: 'inference-provider', + actorUserId: 'user-1', + }); + expect(mapped.unknownTaxonomyValues).toEqual([]); + }); + + test('maps absent AI attribution to other sentinels', () => { + const mapped = mapAiGatewayCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + feature: null, + apiKind: null, + requestedModel: null, + resolvedModel: null, + inferenceProvider: null, + gatewayProvider: null, + totalMicrodollars: 5, + spendRecordCount: 1, + }); + + expect(mapped.driver).toMatchObject({ + productKey: 'other', + featureKey: 'other', + modelOrPlanKey: 'other', + providerKey: 'other', + }); + expect(mapped.unknownTaxonomyValues).toEqual([]); + }); + + test('reports unknown AI and Exa taxonomy while preserving bounded fallback mapping', () => { + const ai = mapAiGatewayCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + feature: 'unregistered-feature', + apiKind: 'unknown-operation', + requestedModel: 'model', + resolvedModel: null, + inferenceProvider: 'provider', + gatewayProvider: null, + totalMicrodollars: 8, + spendRecordCount: 2, + }); + const exa = mapExaCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + path: '/future-path', + totalMicrodollars: 3, + spendRecordCount: 1, + }); + + expect(ai.driver.productKey).toBe('other'); + expect(ai.driver.featureKey).toBe('other'); + expect(ai.unknownTaxonomyValues).toHaveLength(2); + expect(exa.driver).toMatchObject({ + source: 'other', + productKey: 'exa', + featureKey: 'other', + modelOrPlanKey: 'other', + providerKey: 'exa', + }); + expect(exa.unknownTaxonomyValues).toEqual([ + expect.objectContaining({ sourceFamily: 'exa', field: 'feature_key' }), + ]); + }); + + test('keeps allowlisted Exa path as canonical operation key', () => { + const mapped = mapExaCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + path: '/search', + totalMicrodollars: 3, + spendRecordCount: 1, + }); + + expect(mapped.driver.featureKey).toBe('search'); + expect(mapped.unknownTaxonomyValues).toEqual([]); + }); + + test('maps Coding Plan and pure-credit KiloClaw scheduled spend', () => { + const codingPlan = mapCodingPlanCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + termKind: 'renewal', + planId: 'minimax-annual', + providerId: 'minimax', + totalMicrodollars: 100, + spendRecordCount: 1, + }); + const kiloClaw = mapKiloClawCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + isCommit: true, + featureKey: 'renewal', + totalMicrodollars: 200, + spendRecordCount: 1, + }); + + expect(codingPlan.driver).toMatchObject({ + category: 'scheduled', + source: 'coding_plan', + productKey: 'coding-plan', + featureKey: 'renewal', + modelOrPlanKey: 'minimax-annual', + providerKey: 'minimax', + }); + expect(kiloClaw).toMatchObject({ + category: 'scheduled', + source: 'kiloclaw', + productKey: 'kiloclaw-hosting', + featureKey: 'renewal', + modelOrPlanKey: 'commit', + providerKey: 'other', + }); + expect( + mapKiloClawCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + isCommit: false, + featureKey: 'legacy-description', + totalMicrodollars: 1, + spendRecordCount: 1, + }).featureKey + ).toBe('other'); + }); + + test('aggregates identical normalized drivers and owner totals deterministically', async () => { + const input = mapExaCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + path: '/answer', + totalMicrodollars: 4, + spendRecordCount: 1, + }).driver; + const aggregation = await aggregateCanonicalCostInsightDrivers([ + input, + { ...input, totalMicrodollars: 6, spendRecordCount: 2 }, + ]); + + expect(aggregation.totals).toEqual([ + expect.objectContaining({ + owner: userOwner, + category: 'variable', + totalMicrodollars: 10, + spendRecordCount: 3, + }), + ]); + expect(aggregation.drivers).toEqual([ + expect.objectContaining({ + totalMicrodollars: 10, + spendRecordCount: 3, + driverKey: expect.stringMatching(/^[0-9a-f]{64}$/), + }), + ]); + }); + + test('keeps multi-hour canonical source aggregates separated by UTC hour', async () => { + const execute = jest + .fn() + .mockResolvedValueOnce({ + rows: [ + { + hour_start: '2026-06-01 00:00:00+00', + owned_by_user_id: 'user-1', + owned_by_organization_id: null, + actor_user_id: 'user-1', + raw_product_key: null, + raw_feature_key: null, + requested_model: 'model', + resolved_model: null, + inference_provider: 'provider', + gateway_provider: null, + total_microdollars: '4', + spend_record_count: '1', + }, + { + hour_start: '2026-06-01 01:00:00+00', + owned_by_user_id: 'user-1', + owned_by_organization_id: null, + actor_user_id: 'user-1', + raw_product_key: null, + raw_feature_key: null, + requested_model: 'model', + resolved_model: null, + inference_provider: 'provider', + gateway_provider: null, + total_microdollars: '6', + spend_record_count: '2', + }, + ], + }) + .mockResolvedValueOnce({ rows: [] }) + .mockResolvedValueOnce({ rows: [] }) + .mockResolvedValueOnce({ rows: [] }); + const executor = { execute } as unknown as CostInsightQueryExecutor; + + const hourly = await loadCanonicalCostInsightAggregationsByHour(executor, { + startInclusive: '2026-06-01T00:00:00.000Z', + endExclusive: '2026-06-01T02:00:00.000Z', + }); + + expect(execute).toHaveBeenCalledTimes(4); + expect(hourly).toEqual([ + expect.objectContaining({ + hourStart: '2026-06-01T00:00:00.000Z', + totals: [expect.objectContaining({ totalMicrodollars: 4, spendRecordCount: 1 })], + }), + expect.objectContaining({ + hourStart: '2026-06-01T01:00:00.000Z', + totals: [expect.objectContaining({ totalMicrodollars: 6, spendRecordCount: 2 })], + }), + ]); + }); + + test('rejects unsafe database integers instead of rounding them', () => { + expect(() => parseSafeDatabaseInteger('9007199254740992', 'unsafe aggregate')).toThrow( + 'safe-integer range' + ); + }); +}); diff --git a/apps/web/src/lib/cost-insights/canonical-sources.ts b/apps/web/src/lib/cost-insights/canonical-sources.ts new file mode 100644 index 0000000000..391d8052e7 --- /dev/null +++ b/apps/web/src/lib/cost-insights/canonical-sources.ts @@ -0,0 +1,943 @@ +import { GatewayApiKindSchema } from '@kilocode/db'; +import type { CostInsightSpendCategory, CostInsightSpendSource } from '@kilocode/db/schema-types'; +import { + buildCostInsightDriver, + COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, + COST_INSIGHT_DRIVER_FALLBACK, + COST_INSIGHT_EXA_PRODUCT_KEY, + COST_INSIGHT_KILOCLAW_PRODUCT_KEY, + type CostInsightSpendOwner, +} from '@kilocode/db/cost-insights-rollups'; +import { + api_kind, + coding_plan_subscriptions, + coding_plan_terms, + credit_transactions, + exa_usage_log, + feature, + microdollar_usage, + microdollar_usage_metadata, +} from '@kilocode/db/schema'; +import { sql, type SQL } from 'drizzle-orm'; + +import type { db } from '@/lib/drizzle'; +import { EXA_ALLOWED_PATHS, getExaCostInsightFeatureKey } from '@/lib/exa-paths'; +import { validateFeatureHeader } from '@/lib/feature-detection'; + +export const COST_INSIGHT_ROLLUP_VERSION = 1; +export const COST_INSIGHT_OTHER_DRIVER_KEY = COST_INSIGHT_DRIVER_FALLBACK; +export const COST_INSIGHT_DIRECT_GATEWAY_PRODUCT_KEY = 'direct-gateway'; +export { + COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, + COST_INSIGHT_EXA_PRODUCT_KEY, + COST_INSIGHT_KILOCLAW_PRODUCT_KEY, +}; + +export const COST_INSIGHT_EXA_FEATURE_KEYS = EXA_ALLOWED_PATHS; +const HOUR_MS = 60 * 60 * 1_000; +const TIMESTAMP_WITH_TIMEZONE_PATTERN = /(?:Z|[+-]\d{2}(?::?\d{2})?)$/i; + +export type { CostInsightSpendCategory, CostInsightSpendSource }; +export type CostInsightQueryExecutor = Pick; + +export type CanonicalCostInsightDriverInput = { + owner: CostInsightSpendOwner; + category: CostInsightSpendCategory; + source: CostInsightSpendSource; + productKey: string; + featureKey: string; + modelOrPlanKey: string; + providerKey: string; + actorUserId: string; + totalMicrodollars: number; + spendRecordCount: number; +}; + +export type CanonicalCostInsightDriverAggregate = CanonicalCostInsightDriverInput & { + driverKey: string; +}; + +export type CanonicalCostInsightOwnerTotal = { + owner: CostInsightSpendOwner; + category: CostInsightSpendCategory; + totalMicrodollars: number; + spendRecordCount: number; +}; + +export type CostInsightUnknownTaxonomyValue = { + sourceFamily: 'ai_gateway' | 'exa' | 'coding_plan' | 'kiloclaw'; + field: 'product_key' | 'feature_key' | 'term_kind'; + value: string; + spendRecordCount: number; +}; + +export type CanonicalCostInsightAggregation = { + totals: CanonicalCostInsightOwnerTotal[]; + drivers: CanonicalCostInsightDriverAggregate[]; + unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; +}; + +export type CanonicalCostInsightHourAggregation = CanonicalCostInsightAggregation & { + hourStart: string; +}; + +type CanonicalRange = { + startInclusive: string; + endExclusive: string; +}; + +type RawAiAggregate = { + hour_start: string | Date; + owned_by_user_id: string | null; + owned_by_organization_id: string | null; + actor_user_id: string; + raw_product_key: string | null; + raw_feature_key: string | null; + requested_model: string | null; + resolved_model: string | null; + inference_provider: string | null; + gateway_provider: string | null; + total_microdollars: string | number | bigint; + spend_record_count: string | number | bigint; +}; + +type RawExaAggregate = { + hour_start: string | Date; + owned_by_user_id: string | null; + owned_by_organization_id: string | null; + actor_user_id: string; + raw_feature_key: string; + total_microdollars: string | number | bigint; + spend_record_count: string | number | bigint; +}; + +type RawCodingPlanAggregate = { + hour_start: string | Date; + owned_by_user_id: string | null; + owned_by_organization_id: string | null; + actor_user_id: string; + plan_id: string; + provider_id: string; + term_kind: string; + total_microdollars: string | number | bigint; + spend_record_count: string | number | bigint; +}; + +type RawKiloClawAggregate = { + hour_start: string | Date; + owned_by_user_id: string | null; + owned_by_organization_id: string | null; + actor_user_id: string; + feature_key: string; + model_or_plan_key: string; + total_microdollars: string | number | bigint; + spend_record_count: string | number | bigint; +}; + +type RawCanonicalTotal = { + spend_category: CostInsightSpendCategory; + total_microdollars: string | number | bigint; + spend_record_count: string | number | bigint; +}; + +function requireCanonicalRange(range: CanonicalRange): void { + const start = Date.parse(requireUtcTimestamp(range.startInclusive, 'startInclusive')); + const end = Date.parse(requireUtcTimestamp(range.endExclusive, 'endExclusive')); + if (start >= end) { + throw new Error('Cost Insights canonical source range must be a non-empty half-open range.'); + } +} + +export function requireUtcTimestamp(value: string, fieldName: string): string { + const timestamp = Date.parse(value); + if (!TIMESTAMP_WITH_TIMEZONE_PATTERN.test(value) || !Number.isFinite(timestamp)) { + throw new Error(`${fieldName} must be a valid timestamp with an explicit UTC offset.`); + } + return new Date(timestamp).toISOString(); +} + +export function requireUtcHour(value: string, fieldName: string): string { + const timestamp = Date.parse(requireUtcTimestamp(value, fieldName)); + if (timestamp % HOUR_MS !== 0) { + throw new Error(`${fieldName} must be an exact UTC hour.`); + } + return new Date(timestamp).toISOString(); +} + +export function parseSafeDatabaseInteger( + value: string | number | bigint, + fieldName: string +): number { + const parsed = typeof value === 'bigint' ? Number(value) : Number(value); + if (!Number.isSafeInteger(parsed)) { + throw new Error(`${fieldName} is outside the JavaScript safe-integer range.`); + } + return parsed; +} + +function addSafeInteger(left: number, right: number, fieldName: string): number { + const result = left + right; + if (!Number.isSafeInteger(result)) { + throw new Error(`${fieldName} is outside the JavaScript safe-integer range.`); + } + return result; +} + +function normalizeCanonicalHour(value: string | Date): string { + const timestamp = value instanceof Date ? value.getTime() : Date.parse(value); + if (!Number.isFinite(timestamp) || timestamp % HOUR_MS !== 0) { + throw new Error('Canonical Cost Insights source row has an invalid UTC hour.'); + } + return new Date(timestamp).toISOString(); +} + +function ownerFromColumns( + ownedByUserId: string | null, + ownedByOrganizationId: string | null +): CostInsightSpendOwner { + if (ownedByOrganizationId && !ownedByUserId) { + return { type: 'organization', id: ownedByOrganizationId }; + } + if (ownedByUserId && !ownedByOrganizationId) { + return { type: 'user', id: ownedByUserId }; + } + throw new Error('Canonical Cost Insights source row must resolve to exactly one Spend owner.'); +} + +function ownerIdentity(owner: CostInsightSpendOwner): string { + return `${owner.type}:${owner.id}`; +} + +function ownerPredicate(params: { + owner: CostInsightSpendOwner | undefined; + userColumn: SQL; + organizationColumn: SQL; +}): SQL { + if (!params.owner) { + return sql`TRUE`; + } + if (params.owner.type === 'organization') { + return sql`${params.organizationColumn} = ${params.owner.id}`; + } + return sql`${params.organizationColumn} IS NULL AND ${params.userColumn} = ${params.owner.id}`; +} + +function pureCreditKiloClawPredicate(transactionAlias: SQL): SQL { + return sql`( + ${transactionAlias}.credit_category LIKE 'kiloclaw-subscription:%' + OR ${transactionAlias}.credit_category LIKE 'kiloclaw-subscription-commit:%' + )`; +} + +function appendUnknownTaxonomyValue( + target: CostInsightUnknownTaxonomyValue[], + value: CostInsightUnknownTaxonomyValue +): void { + const existing = target.find( + candidate => + candidate.sourceFamily === value.sourceFamily && + candidate.field === value.field && + candidate.value === value.value + ); + if (existing) { + existing.spendRecordCount = addSafeInteger( + existing.spendRecordCount, + value.spendRecordCount, + 'unknown taxonomy spend record count' + ); + return; + } + target.push(value); +} + +export function getAiGatewayCostInsightProductKey(featureValue: string | null): string { + return validateFeatureHeader(featureValue) ?? COST_INSIGHT_OTHER_DRIVER_KEY; +} + +export function getAiGatewayCostInsightFeatureKey(apiKindValue: string | null): string { + const parsedApiKind = GatewayApiKindSchema.safeParse(apiKindValue); + return parsedApiKind.success ? parsedApiKind.data : COST_INSIGHT_OTHER_DRIVER_KEY; +} + +export function mapAiGatewayCanonicalDriver(params: { + owner: CostInsightSpendOwner; + actorUserId: string; + feature: string | null; + apiKind: string | null; + requestedModel: string | null; + resolvedModel: string | null; + inferenceProvider: string | null; + gatewayProvider: string | null; + totalMicrodollars: number; + spendRecordCount: number; +}): { + driver: CanonicalCostInsightDriverInput; + unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; +} { + const unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[] = []; + const productKey = getAiGatewayCostInsightProductKey(params.feature); + if (params.feature && productKey === COST_INSIGHT_OTHER_DRIVER_KEY) { + unknownTaxonomyValues.push({ + sourceFamily: 'ai_gateway', + field: 'product_key', + value: params.feature, + spendRecordCount: params.spendRecordCount, + }); + } + + const featureKey = getAiGatewayCostInsightFeatureKey(params.apiKind); + if (params.apiKind && featureKey === COST_INSIGHT_OTHER_DRIVER_KEY) { + unknownTaxonomyValues.push({ + sourceFamily: 'ai_gateway', + field: 'feature_key', + value: params.apiKind, + spendRecordCount: params.spendRecordCount, + }); + } + + return { + driver: { + owner: params.owner, + category: 'variable', + source: 'ai_gateway', + productKey, + featureKey, + modelOrPlanKey: + params.requestedModel || params.resolvedModel || COST_INSIGHT_OTHER_DRIVER_KEY, + providerKey: + params.inferenceProvider || params.gatewayProvider || COST_INSIGHT_OTHER_DRIVER_KEY, + actorUserId: params.actorUserId, + totalMicrodollars: params.totalMicrodollars, + spendRecordCount: params.spendRecordCount, + }, + unknownTaxonomyValues, + }; +} + +export function mapExaCanonicalDriver(params: { + owner: CostInsightSpendOwner; + actorUserId: string; + path: string; + totalMicrodollars: number; + spendRecordCount: number; +}): { + driver: CanonicalCostInsightDriverInput; + unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; +} { + const featureKey = getExaCostInsightFeatureKey(params.path); + return { + driver: { + owner: params.owner, + category: 'variable', + source: 'other', + productKey: COST_INSIGHT_EXA_PRODUCT_KEY, + featureKey, + modelOrPlanKey: COST_INSIGHT_OTHER_DRIVER_KEY, + providerKey: COST_INSIGHT_EXA_PRODUCT_KEY, + actorUserId: params.actorUserId, + totalMicrodollars: params.totalMicrodollars, + spendRecordCount: params.spendRecordCount, + }, + unknownTaxonomyValues: + featureKey !== COST_INSIGHT_OTHER_DRIVER_KEY + ? [] + : [ + { + sourceFamily: 'exa', + field: 'feature_key', + value: params.path, + spendRecordCount: params.spendRecordCount, + }, + ], + }; +} + +export function mapCodingPlanCanonicalDriver(params: { + owner: CostInsightSpendOwner; + actorUserId: string; + termKind: string; + planId: string; + providerId: string; + totalMicrodollars: number; + spendRecordCount: number; +}): { + driver: CanonicalCostInsightDriverInput; + unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; +} { + const isKnownKind = params.termKind === 'activation' || params.termKind === 'renewal'; + return { + driver: { + owner: params.owner, + category: 'scheduled', + source: 'coding_plan', + productKey: COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, + featureKey: isKnownKind ? params.termKind : COST_INSIGHT_OTHER_DRIVER_KEY, + modelOrPlanKey: params.planId, + providerKey: params.providerId, + actorUserId: params.actorUserId, + totalMicrodollars: params.totalMicrodollars, + spendRecordCount: params.spendRecordCount, + }, + unknownTaxonomyValues: isKnownKind + ? [] + : [ + { + sourceFamily: 'coding_plan', + field: 'term_kind', + value: params.termKind, + spendRecordCount: params.spendRecordCount, + }, + ], + }; +} + +export function mapKiloClawCanonicalDriver(params: { + owner: CostInsightSpendOwner; + actorUserId: string; + isCommit: boolean; + featureKey: string; + totalMicrodollars: number; + spendRecordCount: number; +}): CanonicalCostInsightDriverInput { + return { + owner: params.owner, + category: 'scheduled', + source: 'kiloclaw', + productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, + featureKey: + params.featureKey === 'enrollment' || params.featureKey === 'renewal' + ? params.featureKey + : COST_INSIGHT_OTHER_DRIVER_KEY, + modelOrPlanKey: params.isCommit ? 'commit' : 'standard', + providerKey: COST_INSIGHT_OTHER_DRIVER_KEY, + actorUserId: params.actorUserId, + totalMicrodollars: params.totalMicrodollars, + spendRecordCount: params.spendRecordCount, + }; +} + +function aggregateNormalizedCanonicalCostInsightDrivers( + inputs: CanonicalCostInsightDriverAggregate[] +): { + totals: CanonicalCostInsightOwnerTotal[]; + drivers: CanonicalCostInsightDriverAggregate[]; +} { + const totals = new Map(); + const drivers = new Map(); + + for (const input of inputs) { + if (!Number.isSafeInteger(input.totalMicrodollars) || input.totalMicrodollars <= 0) { + throw new Error('Canonical Cost Insights amount must be a positive safe integer.'); + } + if (!Number.isSafeInteger(input.spendRecordCount) || input.spendRecordCount <= 0) { + throw new Error('Canonical Cost Insights record count must be a positive safe integer.'); + } + + const totalKey = `${ownerIdentity(input.owner)}:${input.category}`; + const priorTotal = totals.get(totalKey); + if (priorTotal) { + priorTotal.totalMicrodollars = addSafeInteger( + priorTotal.totalMicrodollars, + input.totalMicrodollars, + 'canonical total microdollars' + ); + priorTotal.spendRecordCount = addSafeInteger( + priorTotal.spendRecordCount, + input.spendRecordCount, + 'canonical total spend record count' + ); + } else { + totals.set(totalKey, { + owner: input.owner, + category: input.category, + totalMicrodollars: input.totalMicrodollars, + spendRecordCount: input.spendRecordCount, + }); + } + + const driverIdentity = `${totalKey}:${input.driverKey}`; + const priorDriver = drivers.get(driverIdentity); + if (priorDriver) { + priorDriver.totalMicrodollars = addSafeInteger( + priorDriver.totalMicrodollars, + input.totalMicrodollars, + 'canonical driver microdollars' + ); + priorDriver.spendRecordCount = addSafeInteger( + priorDriver.spendRecordCount, + input.spendRecordCount, + 'canonical driver spend record count' + ); + } else { + drivers.set(driverIdentity, { ...input }); + } + } + + return { + totals: [...totals.values()].sort(compareCanonicalTotals), + drivers: [...drivers.values()].sort(compareCanonicalDrivers), + }; +} + +export async function aggregateCanonicalCostInsightDrivers( + inputs: CanonicalCostInsightDriverInput[] +): Promise<{ + totals: CanonicalCostInsightOwnerTotal[]; + drivers: CanonicalCostInsightDriverAggregate[]; +}> { + const normalizedInputs: CanonicalCostInsightDriverAggregate[] = []; + for (const input of inputs) { + const normalizedDriver = await buildCostInsightDriver({ + source: input.source, + productKey: input.productKey, + featureKey: input.featureKey, + modelOrPlanKey: input.modelOrPlanKey, + providerKey: input.providerKey, + actorUserId: input.actorUserId, + }); + normalizedInputs.push({ + ...input, + ...normalizedDriver, + }); + } + return aggregateNormalizedCanonicalCostInsightDrivers(normalizedInputs); +} + +function compareCanonicalTotals( + left: CanonicalCostInsightOwnerTotal, + right: CanonicalCostInsightOwnerTotal +): number { + return ( + ownerIdentity(left.owner).localeCompare(ownerIdentity(right.owner)) || + left.category.localeCompare(right.category) + ); +} + +function compareCanonicalDrivers( + left: CanonicalCostInsightDriverAggregate, + right: CanonicalCostInsightDriverAggregate +): number { + return ( + ownerIdentity(left.owner).localeCompare(ownerIdentity(right.owner)) || + left.category.localeCompare(right.category) || + left.driverKey.localeCompare(right.driverKey) + ); +} + +async function loadAiAggregates( + executor: CostInsightQueryExecutor, + range: CanonicalRange, + owner: CostInsightSpendOwner | undefined +): Promise { + const result = await executor.execute(sql` + SELECT + date_trunc('hour', ${microdollar_usage.created_at}, 'UTC') AS hour_start, + CASE WHEN ${microdollar_usage.organization_id} IS NULL + THEN ${microdollar_usage.kilo_user_id} ELSE NULL END AS owned_by_user_id, + ${microdollar_usage.organization_id} AS owned_by_organization_id, + ${microdollar_usage.kilo_user_id} AS actor_user_id, + ${feature.feature} AS raw_product_key, + ${api_kind.api_kind} AS raw_feature_key, + ${microdollar_usage.requested_model} AS requested_model, + ${microdollar_usage.model} AS resolved_model, + ${microdollar_usage.inference_provider} AS inference_provider, + ${microdollar_usage.provider} AS gateway_provider, + SUM(${microdollar_usage.cost})::text AS total_microdollars, + COUNT(*)::text AS spend_record_count + FROM ${microdollar_usage} + LEFT JOIN ${microdollar_usage_metadata} + ON ${microdollar_usage_metadata.id} = ${microdollar_usage.id} + LEFT JOIN ${feature} + ON ${feature.feature_id} = ${microdollar_usage_metadata.feature_id} + LEFT JOIN ${api_kind} + ON ${api_kind.api_kind_id} = ${microdollar_usage_metadata.api_kind_id} + WHERE ${microdollar_usage.created_at} >= ${range.startInclusive} + AND ${microdollar_usage.created_at} < ${range.endExclusive} + AND ${microdollar_usage.cost} > 0 + AND ${ownerPredicate({ + owner, + userColumn: sql`${microdollar_usage.kilo_user_id}`, + organizationColumn: sql`${microdollar_usage.organization_id}`, + })} + GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + `); + return result.rows; +} + +async function loadExaAggregates( + executor: CostInsightQueryExecutor, + range: CanonicalRange, + owner: CostInsightSpendOwner | undefined +): Promise { + const result = await executor.execute(sql` + SELECT + date_trunc('hour', ${exa_usage_log.created_at}, 'UTC') AS hour_start, + CASE WHEN ${exa_usage_log.organization_id} IS NULL + THEN ${exa_usage_log.kilo_user_id} ELSE NULL END AS owned_by_user_id, + ${exa_usage_log.organization_id} AS owned_by_organization_id, + ${exa_usage_log.kilo_user_id} AS actor_user_id, + ${exa_usage_log.path} AS raw_feature_key, + SUM(${exa_usage_log.cost_microdollars})::text AS total_microdollars, + COUNT(*)::text AS spend_record_count + FROM ${exa_usage_log} + WHERE ${exa_usage_log.created_at} >= ${range.startInclusive} + AND ${exa_usage_log.created_at} < ${range.endExclusive} + AND ${exa_usage_log.charged_to_balance} = TRUE + AND ${exa_usage_log.cost_microdollars} > 0 + AND ${ownerPredicate({ + owner, + userColumn: sql`${exa_usage_log.kilo_user_id}`, + organizationColumn: sql`${exa_usage_log.organization_id}`, + })} + GROUP BY 1, 2, 3, 4, 5 + `); + return result.rows; +} + +async function loadCodingPlanAggregates( + executor: CostInsightQueryExecutor, + range: CanonicalRange, + owner: CostInsightSpendOwner | undefined +): Promise { + const result = await executor.execute(sql` + SELECT + date_trunc('hour', ${credit_transactions.created_at}, 'UTC') AS hour_start, + CASE WHEN ${credit_transactions.organization_id} IS NULL + THEN ${credit_transactions.kilo_user_id} ELSE NULL END AS owned_by_user_id, + ${credit_transactions.organization_id} AS owned_by_organization_id, + ${coding_plan_terms.user_id} AS actor_user_id, + ${coding_plan_terms.plan_id} AS plan_id, + ${coding_plan_subscriptions.provider_id} AS provider_id, + ${coding_plan_terms.kind} AS term_kind, + SUM(-${credit_transactions.amount_microdollars})::text AS total_microdollars, + COUNT(*)::text AS spend_record_count + FROM ${coding_plan_terms} + INNER JOIN ${credit_transactions} + ON ${credit_transactions.id} = ${coding_plan_terms.credit_transaction_id} + INNER JOIN ${coding_plan_subscriptions} + ON ${coding_plan_subscriptions.id} = ${coding_plan_terms.subscription_id} + WHERE ${credit_transactions.created_at} >= ${range.startInclusive} + AND ${credit_transactions.created_at} < ${range.endExclusive} + AND ${credit_transactions.amount_microdollars} < 0 + AND ${ownerPredicate({ + owner, + userColumn: sql`${credit_transactions.kilo_user_id}`, + organizationColumn: sql`${credit_transactions.organization_id}`, + })} + GROUP BY 1, 2, 3, 4, 5, 6, 7 + `); + return result.rows; +} + +async function loadKiloClawAggregates( + executor: CostInsightQueryExecutor, + range: CanonicalRange, + owner: CostInsightSpendOwner | undefined +): Promise { + const transactionAlias = sql.raw('ct'); + const result = await executor.execute(sql` + WITH matching_transactions AS ( + SELECT + date_trunc('hour', ct.created_at, 'UTC') AS hour_start, + CASE WHEN ct.organization_id IS NULL THEN ct.kilo_user_id ELSE NULL END + AS owned_by_user_id, + ct.organization_id AS owned_by_organization_id, + ct.kilo_user_id AS actor_user_id, + CASE WHEN ct.credit_category LIKE 'kiloclaw-subscription-commit:%' + THEN 'commit' ELSE 'standard' END AS model_or_plan_key, + CASE + WHEN ct.description IN ( + 'KiloClaw standard enrollment', + 'KiloClaw commit enrollment' + ) THEN 'enrollment' + WHEN ct.description IN ( + 'KiloClaw standard renewal', + 'KiloClaw commit renewal' + ) THEN 'renewal' + ELSE 'other' + END AS feature_key, + -ct.amount_microdollars AS amount_microdollars + FROM ${credit_transactions} ct + WHERE ct.created_at >= ${range.startInclusive} + AND ct.created_at < ${range.endExclusive} + AND ct.amount_microdollars < 0 + AND ${pureCreditKiloClawPredicate(transactionAlias)} + AND ${ownerPredicate({ + owner, + userColumn: sql.raw('ct.kilo_user_id'), + organizationColumn: sql.raw('ct.organization_id'), + })} + ) + SELECT + hour_start, + owned_by_user_id, + owned_by_organization_id, + actor_user_id, + feature_key, + model_or_plan_key, + SUM(amount_microdollars)::text AS total_microdollars, + COUNT(*)::text AS spend_record_count + FROM matching_transactions + GROUP BY 1, 2, 3, 4, 5, 6 + `); + return result.rows; +} + +type CanonicalHourAccumulator = { + inputs: CanonicalCostInsightDriverInput[]; + unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; +}; + +function getCanonicalHourAccumulator( + accumulators: Map, + rawHourStart: string | Date +): CanonicalHourAccumulator { + const hourStart = normalizeCanonicalHour(rawHourStart); + const existing = accumulators.get(hourStart); + if (existing) return existing; + const created = { inputs: [], unknownTaxonomyValues: [] }; + accumulators.set(hourStart, created); + return created; +} + +function appendMappedCanonicalDriver( + accumulator: CanonicalHourAccumulator, + mapped: { + driver: CanonicalCostInsightDriverInput; + unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; + } +): void { + accumulator.inputs.push(mapped.driver); + for (const unknown of mapped.unknownTaxonomyValues) { + appendUnknownTaxonomyValue(accumulator.unknownTaxonomyValues, unknown); + } +} + +export async function loadCanonicalCostInsightAggregationsByHour( + executor: CostInsightQueryExecutor, + params: CanonicalRange & { owner?: CostInsightSpendOwner } +): Promise { + requireCanonicalRange(params); + const accumulators = new Map(); + + const aiRows = await loadAiAggregates(executor, params, params.owner); + for (const row of aiRows) { + appendMappedCanonicalDriver( + getCanonicalHourAccumulator(accumulators, row.hour_start), + mapAiGatewayCanonicalDriver({ + owner: ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id), + actorUserId: row.actor_user_id, + feature: row.raw_product_key, + apiKind: row.raw_feature_key, + requestedModel: row.requested_model, + resolvedModel: row.resolved_model, + inferenceProvider: row.inference_provider, + gatewayProvider: row.gateway_provider, + totalMicrodollars: parseSafeDatabaseInteger( + row.total_microdollars, + 'AI Gateway canonical microdollars' + ), + spendRecordCount: parseSafeDatabaseInteger( + row.spend_record_count, + 'AI Gateway canonical record count' + ), + }) + ); + } + + const exaRows = await loadExaAggregates(executor, params, params.owner); + for (const row of exaRows) { + appendMappedCanonicalDriver( + getCanonicalHourAccumulator(accumulators, row.hour_start), + mapExaCanonicalDriver({ + owner: ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id), + actorUserId: row.actor_user_id, + path: row.raw_feature_key, + totalMicrodollars: parseSafeDatabaseInteger( + row.total_microdollars, + 'Exa canonical microdollars' + ), + spendRecordCount: parseSafeDatabaseInteger( + row.spend_record_count, + 'Exa canonical record count' + ), + }) + ); + } + + const codingPlanRows = await loadCodingPlanAggregates(executor, params, params.owner); + for (const row of codingPlanRows) { + appendMappedCanonicalDriver( + getCanonicalHourAccumulator(accumulators, row.hour_start), + mapCodingPlanCanonicalDriver({ + owner: ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id), + actorUserId: row.actor_user_id, + termKind: row.term_kind, + planId: row.plan_id, + providerId: row.provider_id, + totalMicrodollars: parseSafeDatabaseInteger( + row.total_microdollars, + 'Coding Plan canonical microdollars' + ), + spendRecordCount: parseSafeDatabaseInteger( + row.spend_record_count, + 'Coding Plan canonical record count' + ), + }) + ); + } + + const kiloClawRows = await loadKiloClawAggregates(executor, params, params.owner); + for (const row of kiloClawRows) { + getCanonicalHourAccumulator(accumulators, row.hour_start).inputs.push( + mapKiloClawCanonicalDriver({ + owner: ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id), + actorUserId: row.actor_user_id, + isCommit: row.model_or_plan_key === 'commit', + featureKey: row.feature_key, + totalMicrodollars: parseSafeDatabaseInteger( + row.total_microdollars, + 'KiloClaw canonical microdollars' + ), + spendRecordCount: parseSafeDatabaseInteger( + row.spend_record_count, + 'KiloClaw canonical record count' + ), + }) + ); + } + + const hourly: CanonicalCostInsightHourAggregation[] = []; + for (const hourStart of [...accumulators.keys()].sort()) { + const accumulator = accumulators.get(hourStart); + if (!accumulator) continue; + hourly.push({ + hourStart, + ...(await aggregateCanonicalCostInsightDrivers(accumulator.inputs)), + unknownTaxonomyValues: accumulator.unknownTaxonomyValues, + }); + } + return hourly; +} + +export async function loadCanonicalCostInsightAggregation( + executor: CostInsightQueryExecutor, + params: CanonicalRange & { owner?: CostInsightSpendOwner } +): Promise { + const hourly = await loadCanonicalCostInsightAggregationsByHour(executor, params); + const unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[] = []; + for (const hour of hourly) { + for (const unknown of hour.unknownTaxonomyValues) { + appendUnknownTaxonomyValue(unknownTaxonomyValues, unknown); + } + } + return { + ...aggregateNormalizedCanonicalCostInsightDrivers(hourly.flatMap(hour => hour.drivers)), + unknownTaxonomyValues, + }; +} + +export async function getCanonicalOwnerSpendTotals( + executor: CostInsightQueryExecutor, + params: CanonicalRange & { owner: CostInsightSpendOwner } +): Promise<{ + variableMicrodollars: number; + scheduledMicrodollars: number; + variableRecordCount: number; + scheduledRecordCount: number; +}> { + requireCanonicalRange(params); + const owner = params.owner; + const kiloClawAlias = sql.raw('ct'); + const result = await executor.execute(sql` + WITH canonical_totals AS ( + SELECT + 'variable'::text AS spend_category, + SUM(${microdollar_usage.cost}) AS total_microdollars, + COUNT(*) AS spend_record_count + FROM ${microdollar_usage} + WHERE ${microdollar_usage.created_at} >= ${params.startInclusive} + AND ${microdollar_usage.created_at} < ${params.endExclusive} + AND ${microdollar_usage.cost} > 0 + AND ${ownerPredicate({ + owner, + userColumn: sql`${microdollar_usage.kilo_user_id}`, + organizationColumn: sql`${microdollar_usage.organization_id}`, + })} + UNION ALL + SELECT + 'variable'::text, + SUM(${exa_usage_log.cost_microdollars}), + COUNT(*) + FROM ${exa_usage_log} + WHERE ${exa_usage_log.created_at} >= ${params.startInclusive} + AND ${exa_usage_log.created_at} < ${params.endExclusive} + AND ${exa_usage_log.charged_to_balance} = TRUE + AND ${exa_usage_log.cost_microdollars} > 0 + AND ${ownerPredicate({ + owner, + userColumn: sql`${exa_usage_log.kilo_user_id}`, + organizationColumn: sql`${exa_usage_log.organization_id}`, + })} + UNION ALL + SELECT + 'scheduled'::text, + SUM(-${credit_transactions.amount_microdollars}), + COUNT(*) + FROM ${coding_plan_terms} + INNER JOIN ${credit_transactions} + ON ${credit_transactions.id} = ${coding_plan_terms.credit_transaction_id} + WHERE ${credit_transactions.created_at} >= ${params.startInclusive} + AND ${credit_transactions.created_at} < ${params.endExclusive} + AND ${credit_transactions.amount_microdollars} < 0 + AND ${ownerPredicate({ + owner, + userColumn: sql`${credit_transactions.kilo_user_id}`, + organizationColumn: sql`${credit_transactions.organization_id}`, + })} + UNION ALL + SELECT + 'scheduled'::text, + SUM(-ct.amount_microdollars), + COUNT(*) + FROM ${credit_transactions} ct + WHERE ct.created_at >= ${params.startInclusive} + AND ct.created_at < ${params.endExclusive} + AND ct.amount_microdollars < 0 + AND ${pureCreditKiloClawPredicate(kiloClawAlias)} + AND ${ownerPredicate({ + owner, + userColumn: sql.raw('ct.kilo_user_id'), + organizationColumn: sql.raw('ct.organization_id'), + })} + ) + SELECT + spend_category, + COALESCE(SUM(total_microdollars), 0)::text AS total_microdollars, + COALESCE(SUM(spend_record_count), 0)::text AS spend_record_count + FROM canonical_totals + GROUP BY spend_category + `); + + let variableMicrodollars = 0; + let scheduledMicrodollars = 0; + let variableRecordCount = 0; + let scheduledRecordCount = 0; + for (const row of result.rows) { + const amount = parseSafeDatabaseInteger(row.total_microdollars, 'canonical owner microdollars'); + const count = parseSafeDatabaseInteger(row.spend_record_count, 'canonical owner record count'); + if (row.spend_category === 'variable') { + variableMicrodollars = amount; + variableRecordCount = count; + } else if (row.spend_category === 'scheduled') { + scheduledMicrodollars = amount; + scheduledRecordCount = count; + } + } + return { + variableMicrodollars, + scheduledMicrodollars, + variableRecordCount, + scheduledRecordCount, + }; +} diff --git a/apps/web/src/lib/cost-insights/cost-insights-rollups-script.test.ts b/apps/web/src/lib/cost-insights/cost-insights-rollups-script.test.ts new file mode 100644 index 0000000000..d2c02d8f89 --- /dev/null +++ b/apps/web/src/lib/cost-insights/cost-insights-rollups-script.test.ts @@ -0,0 +1,93 @@ +import { parseCostInsightRollupScriptArgs } from '@/scripts/db/cost-insights-rollups'; + +describe('Cost Insights rollup operator arguments', () => { + test('defaults to dry-run reconciliation', () => { + expect( + parseCostInsightRollupScriptArgs([ + '--start-hour', + '2026-06-01T00:00:00.000Z', + '--end-hour', + '2026-06-01T02:00:00.000Z', + '--max-hours', + '2', + ]) + ).toEqual({ + execute: false, + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + maxHours: 2, + sleepMs: 0, + }); + }); + + test('parses explicit execution and pacing', () => { + expect( + parseCostInsightRollupScriptArgs([ + '--execute', + '--start-hour', + '2026-06-01T00:00:00.000Z', + '--end-hour', + '2026-06-01T02:00:00.000Z', + '--max-hours', + '24', + '--sleep-ms', + '250', + ]) + ).toMatchObject({ execute: true, maxHours: 24, sleepMs: 250 }); + }); + + test('parses one-time live-capture coverage initialization only for execution', () => { + expect( + parseCostInsightRollupScriptArgs([ + '--execute', + '--live-capture-start-hour', + '2026-06-01T02:00:00.000Z', + '--start-hour', + '2026-06-01T00:00:00.000Z', + '--end-hour', + '2026-06-01T02:00:00.000Z', + '--max-hours', + '2', + ]) + ).toMatchObject({ liveCaptureStartHour: '2026-06-01T02:00:00.000Z' }); + + expect(() => + parseCostInsightRollupScriptArgs([ + '--live-capture-start-hour', + '2026-06-01T02:00:00.000Z', + '--start-hour', + '2026-06-01T00:00:00.000Z', + '--end-hour', + '2026-06-01T02:00:00.000Z', + '--max-hours', + '2', + ]) + ).toThrow('--live-capture-start-hour requires --execute'); + }); + + test('rejects ranges beyond explicit maximum', () => { + expect(() => + parseCostInsightRollupScriptArgs([ + '--start-hour', + '2026-06-01T00:00:00.000Z', + '--end-hour', + '2026-06-01T03:00:00.000Z', + '--max-hours', + '2', + ]) + ).toThrow('Requested range must contain 1-2 UTC hours'); + }); + + test('rejects non-hour-aligned bounds', () => { + expect(() => + parseCostInsightRollupScriptArgs([ + '--start-hour', + '2026-06-01T00:30:00.000Z', + '--end-hour', + '2026-06-01T02:00:00.000Z', + '--max-hours', + '2', + ]) + ).toThrow('exact UTC hour'); + }); +}); diff --git a/apps/web/src/lib/cost-insights/rollup-maintenance.integration.test.ts b/apps/web/src/lib/cost-insights/rollup-maintenance.integration.test.ts new file mode 100644 index 0000000000..6ade59cd6d --- /dev/null +++ b/apps/web/src/lib/cost-insights/rollup-maintenance.integration.test.ts @@ -0,0 +1,320 @@ +import { afterEach, describe, expect, test } from '@jest/globals'; +import { and, eq } from 'drizzle-orm'; + +import { db } from '@/lib/drizzle'; +import { + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, + cost_insight_rollup_coverage, + cost_insight_rollup_degraded_intervals, + kilocode_users, + microdollar_usage, +} from '@kilocode/db/schema'; +import { captureCostInsightSpend } from '@kilocode/db/cost-insights-rollups'; + +import { + backfillCostInsightHour, + backfillCostInsightRollupsNewestFirst, + initializeCostInsightRollupCoverage, + recordCostInsightDegradedInterval, + reconcileCostInsightRollups, + repairOwnerSpendRollups, + resolveCostInsightDegradedInterval, +} from './rollup-maintenance'; + +const userIds = new Set(); + +async function createUser(): Promise { + const id = `cost-insights-maintenance-${crypto.randomUUID()}`; + userIds.add(id); + await db.insert(kilocode_users).values({ + id, + google_user_email: `${id}@example.com`, + google_user_name: 'Cost Insights Maintenance Test', + google_user_image_url: 'https://example.com/avatar.png', + stripe_customer_id: `cus_${crypto.randomUUID()}`, + }); + return id; +} + +function rawUsage(userId: string, cost: number, createdAt: string) { + return { + kilo_user_id: userId, + cost, + input_tokens: 0, + output_tokens: 0, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: createdAt, + provider: 'provider', + model: 'model', + }; +} + +afterEach(async () => { + await db + .delete(cost_insight_rollup_degraded_intervals) + .where(eq(cost_insight_rollup_degraded_intervals.reason, 'reconciliation_mismatch')); + await db + .delete(cost_insight_rollup_coverage) + .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); + for (const userId of userIds) { + await db + .delete(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); + await db + .delete(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); + await db.delete(microdollar_usage).where(eq(microdollar_usage.kilo_user_id, userId)); + await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); + } + userIds.clear(); +}); + +describe('Cost Insights rollup maintenance integration', () => { + test('initializes coverage and rebuilds a canonical hour idempotently', async () => { + const userId = await createUser(); + await db.insert(microdollar_usage).values(rawUsage(userId, 11, '2026-06-01T01:30:00.000Z')); + await initializeCostInsightRollupCoverage(db, '2026-06-01T02:00:00.000Z'); + + const first = await backfillCostInsightHour(db, '2026-06-01T01:00:00.000Z'); + const second = await backfillCostInsightHour(db, '2026-06-01T01:00:00.000Z'); + + expect(first).toMatchObject({ + canonicalMicrodollars: 11, + canonicalSpendRecordCount: 1, + coverageAdvanced: true, + }); + expect(second).toMatchObject({ + canonicalMicrodollars: 11, + canonicalSpendRecordCount: 1, + coverageAdvanced: false, + }); + const [coverage] = await db + .select() + .from(cost_insight_rollup_coverage) + .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); + expect(new Date(coverage.coverage_start_hour ?? '').toISOString()).toBe( + '2026-06-01T01:00:00.000Z' + ); + const totals = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); + const drivers = await db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); + expect(totals).toEqual([ + expect.objectContaining({ total_microdollars: 11, spend_record_count: 1 }), + ]); + expect(drivers).toEqual([ + expect.objectContaining({ total_microdollars: 11, spend_record_count: 1 }), + ]); + }); + + test('stages multi-hour canonical sources once and persists newest-first results', async () => { + const userId = await createUser(); + await db + .insert(microdollar_usage) + .values([ + rawUsage(userId, 5, '2026-06-01T00:30:00.000Z'), + rawUsage(userId, 7, '2026-06-01T01:30:00.000Z'), + ]); + await initializeCostInsightRollupCoverage(db, '2026-06-01T02:00:00.000Z'); + + const results = await backfillCostInsightRollupsNewestFirst(db, { + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + maxHours: 2, + }); + + expect(results.map(result => result.hourStart)).toEqual([ + '2026-06-01T01:00:00.000Z', + '2026-06-01T00:00:00.000Z', + ]); + const totals = await db + .select({ + hourStart: cost_insight_owner_hour_totals.hour_start, + amount: cost_insight_owner_hour_totals.total_microdollars, + }) + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)) + .orderBy(cost_insight_owner_hour_totals.hour_start); + expect( + totals.map(row => ({ hourStart: new Date(row.hourStart).toISOString(), amount: row.amount })) + ).toEqual([ + { hourStart: '2026-06-01T00:00:00.000Z', amount: 5 }, + { hourStart: '2026-06-01T01:00:00.000Z', amount: 7 }, + ]); + }); + + test('rejects bulk replacement at or after live-capture cutover', async () => { + const userId = await createUser(); + await db.insert(microdollar_usage).values(rawUsage(userId, 13, '2026-06-01T02:30:00.000Z')); + await initializeCostInsightRollupCoverage(db, '2026-06-01T02:00:00.000Z'); + + await expect(backfillCostInsightHour(db, '2026-06-01T02:00:00.000Z')).rejects.toThrow( + 'restricted to pre-cutover hours' + ); + + const totals = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); + expect(totals).toHaveLength(0); + }); + + test('targeted repair deletes stale rows when canonical owner-hour spend is zero', async () => { + const userId = await createUser(); + await db.insert(cost_insight_owner_hour_totals).values({ + owned_by_user_id: userId, + hour_start: '2026-06-01T00:00:00.000Z', + spend_category: 'variable', + total_microdollars: 99, + spend_record_count: 1, + }); + await db.insert(cost_insight_owner_hour_driver_buckets).values({ + owned_by_user_id: userId, + hour_start: '2026-06-01T00:00:00.000Z', + spend_category: 'variable', + driver_key: 'a'.repeat(64), + source: 'ai_gateway', + product_key: 'other', + feature_key: 'other', + model_or_plan_key: 'model', + provider_key: 'provider', + actor_user_id: userId, + total_microdollars: 99, + spend_record_count: 1, + }); + + await repairOwnerSpendRollups(db, { + owner: { type: 'user', id: userId }, + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T01:00:00.000Z', + maxHours: 1, + }); + + const totals = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); + const drivers = await db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); + expect(totals).toHaveLength(0); + expect(drivers).toHaveLength(0); + }); + + test('reconciles multi-hour ranges in bounded repeatable-read chunks', async () => { + await db.insert(cost_insight_rollup_coverage).values({ + rollup_version: 1, + live_capture_start_hour: '2026-06-01T02:00:00.000Z', + coverage_start_hour: '2026-06-01T00:00:00.000Z', + }); + let transactionCount = 0; + const countingDatabase = new Proxy(db, { + get(target, property, receiver) { + if (property !== 'transaction') return Reflect.get(target, property, receiver); + return (...args: unknown[]) => { + transactionCount++; + return Reflect.apply(target.transaction, target, args); + }; + }, + }); + + const report = await reconcileCostInsightRollups(countingDatabase, { + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + maxHours: 2, + chunkHours: 1, + }); + + expect(transactionCount).toBe(2); + expect(report).toMatchObject({ + checkedHourCount: 2, + mismatchCount: 0, + detailsTruncated: false, + }); + }); + + test('records degraded intervals idempotently and resolves them explicitly', async () => { + const params = { + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + reason: 'reconciliation_mismatch' as const, + }; + const firstId = await recordCostInsightDegradedInterval(db, params); + const secondId = await recordCostInsightDegradedInterval(db, params); + const overlappingId = await recordCostInsightDegradedInterval(db, { + ...params, + startHour: '2026-06-01T01:00:00.000Z', + endHourExclusive: '2026-06-01T03:00:00.000Z', + }); + expect(secondId).toBe(firstId); + expect(overlappingId).toBe(firstId); + + const unresolved = await db.select().from(cost_insight_rollup_degraded_intervals); + expect(unresolved).toEqual([ + expect.objectContaining({ + id: firstId, + start_hour: expect.any(String), + end_hour_exclusive: expect.any(String), + }), + ]); + expect(new Date(unresolved[0].start_hour).toISOString()).toBe('2026-06-01T00:00:00.000Z'); + expect(new Date(unresolved[0].end_hour_exclusive).toISOString()).toBe( + '2026-06-01T03:00:00.000Z' + ); + + await resolveCostInsightDegradedInterval(db, firstId); + const [resolved] = await db + .select() + .from(cost_insight_rollup_degraded_intervals) + .where(eq(cost_insight_rollup_degraded_intervals.id, firstId)); + expect(resolved.resolved_at).not.toBeNull(); + }); + + test('serializes concurrent live capture and owner repair without losing spend', async () => { + const userId = await createUser(); + const owner = { type: 'user', id: userId } as const; + const occurredAt = '2026-06-01T00:30:00.000Z'; + + await Promise.all([ + repairOwnerSpendRollups(db, { + owner, + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T01:00:00.000Z', + maxHours: 1, + }), + db.transaction(async transaction => { + await transaction.insert(microdollar_usage).values(rawUsage(userId, 5, occurredAt)); + await captureCostInsightSpend(transaction, { + owner, + actorUserId: userId, + occurredAt, + amountMicrodollars: 5, + category: 'variable', + source: 'ai_gateway', + productKey: 'other', + featureKey: 'other', + modelOrPlanKey: 'model', + providerKey: 'provider', + }); + }), + ]); + + const [total] = await db + .select() + .from(cost_insight_owner_hour_totals) + .where( + and( + eq(cost_insight_owner_hour_totals.owned_by_user_id, userId), + eq(cost_insight_owner_hour_totals.hour_start, '2026-06-01T00:00:00.000Z') + ) + ); + expect(total).toMatchObject({ total_microdollars: 5, spend_record_count: 1 }); + }); +}); diff --git a/apps/web/src/lib/cost-insights/rollup-maintenance.test.ts b/apps/web/src/lib/cost-insights/rollup-maintenance.test.ts new file mode 100644 index 0000000000..ed0c70142d --- /dev/null +++ b/apps/web/src/lib/cost-insights/rollup-maintenance.test.ts @@ -0,0 +1,156 @@ +import type { CanonicalCostInsightOwnerTotal } from './canonical-sources'; +import { + assertCanonicalTotalsMatchDrivers, + compareCostInsightHourAggregates, +} from './rollup-maintenance'; + +const hourStart = '2026-06-01T00:00:00.000Z'; +const owner = { type: 'user', id: 'user-1' } as const; +const total: CanonicalCostInsightOwnerTotal = { + owner, + category: 'variable', + totalMicrodollars: 10, + spendRecordCount: 2, +}; + +function rollupValue(params: { amount: number; count: number }) { + return { + owner, + category: 'variable' as const, + totalMicrodollars: params.amount, + spendRecordCount: params.count, + }; +} + +describe('Cost Insights rollup maintenance', () => { + test('accepts canonical totals that equal combined driver sums', () => { + expect(() => + assertCanonicalTotalsMatchDrivers({ + totals: [total], + drivers: [ + { + owner, + category: 'variable', + source: 'other', + productKey: 'exa', + featureKey: 'search', + modelOrPlanKey: 'other', + providerKey: 'exa', + actorUserId: 'user-1', + totalMicrodollars: 4, + spendRecordCount: 1, + driverKey: 'a'.repeat(64), + }, + { + owner, + category: 'variable', + source: 'ai_gateway', + productKey: 'direct-gateway', + featureKey: 'responses', + modelOrPlanKey: 'model', + providerKey: 'provider', + actorUserId: 'user-1', + totalMicrodollars: 6, + spendRecordCount: 1, + driverKey: 'b'.repeat(64), + }, + ], + }) + ).not.toThrow(); + }); + + test('rejects canonical totals that differ from driver sums', () => { + expect(() => + assertCanonicalTotalsMatchDrivers({ + totals: [total], + drivers: [ + { + owner, + category: 'variable', + source: 'other', + productKey: 'exa', + featureKey: 'search', + modelOrPlanKey: 'other', + providerKey: 'exa', + actorUserId: 'user-1', + totalMicrodollars: 9, + spendRecordCount: 2, + driverKey: 'a'.repeat(64), + }, + ], + }) + ).toThrow('totals do not equal combined driver sums'); + }); + + test('reports missing total, driver sum, and unknown taxonomy mismatch classes', () => { + const mismatches = compareCostInsightHourAggregates({ + hourStart, + canonicalTotals: [total], + persistedTotals: new Map(), + persistedDriverSums: new Map(), + unknownTaxonomyValues: [ + { + sourceFamily: 'exa', + field: 'feature_key', + value: '/new-operation', + spendRecordCount: 1, + }, + ], + }); + + expect(mismatches.map(mismatch => mismatch.type)).toEqual([ + 'missing_total', + 'record_count_difference', + 'driver_sum_difference', + 'unknown_taxonomy_value', + ]); + }); + + test('reports amount, record-count, and driver-sum differences independently', () => { + const mismatches = compareCostInsightHourAggregates({ + hourStart, + canonicalTotals: [total], + persistedTotals: new Map([['user:user-1:variable', rollupValue({ amount: 8, count: 1 })]]), + persistedDriverSums: new Map([ + ['user:user-1:variable', rollupValue({ amount: 7, count: 1 })], + ]), + }); + + expect(mismatches).toEqual([ + expect.objectContaining({ + type: 'amount_difference', + expectedMicrodollars: 10, + actualMicrodollars: 8, + }), + expect.objectContaining({ + type: 'record_count_difference', + expectedRecordCount: 2, + actualRecordCount: 1, + }), + expect.objectContaining({ + type: 'driver_sum_difference', + expectedMicrodollars: 10, + actualMicrodollars: 7, + expectedRecordCount: 2, + actualRecordCount: 1, + }), + ]); + }); + + test('reports inflated orphan rollup rows against a zero canonical source sum', () => { + const mismatches = compareCostInsightHourAggregates({ + hourStart, + canonicalTotals: [], + persistedTotals: new Map([['user:user-1:variable', rollupValue({ amount: 10, count: 2 })]]), + persistedDriverSums: new Map([ + ['user:user-1:variable', rollupValue({ amount: 10, count: 2 })], + ]), + }); + + expect(mismatches.map(mismatch => mismatch.type)).toEqual([ + 'amount_difference', + 'record_count_difference', + 'driver_sum_difference', + ]); + }); +}); diff --git a/apps/web/src/lib/cost-insights/rollup-maintenance.ts b/apps/web/src/lib/cost-insights/rollup-maintenance.ts new file mode 100644 index 0000000000..0b813fa9f9 --- /dev/null +++ b/apps/web/src/lib/cost-insights/rollup-maintenance.ts @@ -0,0 +1,1122 @@ +import { + acquireCostInsightOwnerHourLock, + type CostInsightSpendOwner, +} from '@kilocode/db/cost-insights-rollups'; +import { + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, + cost_insight_rollup_coverage, + cost_insight_rollup_degraded_intervals, +} from '@kilocode/db/schema'; +import type { CostInsightRollupDegradedReason } from '@kilocode/db/schema-types'; +import { and, asc, eq, gte, inArray, isNull, lte, or, sql, type SQL } from 'drizzle-orm'; + +import { type DrizzleTransaction, type db } from '@/lib/drizzle'; + +import { + COST_INSIGHT_ROLLUP_VERSION, + loadCanonicalCostInsightAggregation, + loadCanonicalCostInsightAggregationsByHour, + parseSafeDatabaseInteger, + requireUtcHour, + type CanonicalCostInsightAggregation, + type CanonicalCostInsightDriverAggregate, + type CanonicalCostInsightOwnerTotal, + type CostInsightSpendCategory, + type CostInsightSpendSource, + type CostInsightUnknownTaxonomyValue, +} from './canonical-sources'; +import { getCostInsightRollupCoverage } from './spend-repository'; + +const HOUR_MS = 60 * 60 * 1_000; +const INSERT_BATCH_SIZE = 500; +const BACKFILL_SOURCE_CHUNK_HOURS = 24; +const MAX_RECONCILIATION_MISMATCH_DETAILS = 1_000; +const DEFAULT_RECONCILIATION_CHUNK_HOURS = 24; +const MAX_RECONCILIATION_CHUNK_HOURS = 24; +const MAINTENANCE_STATEMENT_TIMEOUT_MS = 120_000; +const MAINTENANCE_LOCK_TIMEOUT_MS = 5_000; + +export type CostInsightMaintenanceDatabase = Pick; + +export type CostInsightHourReplacementResult = { + hourStart: string; + totalRowCount: number; + driverRowCount: number; + canonicalSpendRecordCount: number; + canonicalMicrodollars: number; + coverageAdvanced: boolean; + durationMs: number; +}; + +export type CostInsightReconciliationMismatch = + | { + type: 'missing_total'; + hourStart: string; + owner: CostInsightSpendOwner; + category: CostInsightSpendCategory; + expectedMicrodollars: number; + expectedRecordCount: number; + } + | { + type: 'amount_difference'; + hourStart: string; + owner: CostInsightSpendOwner; + category: CostInsightSpendCategory; + expectedMicrodollars: number; + actualMicrodollars: number; + } + | { + type: 'record_count_difference'; + hourStart: string; + owner: CostInsightSpendOwner; + category: CostInsightSpendCategory; + expectedRecordCount: number; + actualRecordCount: number; + } + | { + type: 'driver_sum_difference'; + hourStart: string; + owner: CostInsightSpendOwner; + category: CostInsightSpendCategory; + expectedMicrodollars: number; + actualMicrodollars: number; + expectedRecordCount: number; + actualRecordCount: number; + } + | { + type: 'unknown_taxonomy_value'; + hourStart: string; + value: CostInsightUnknownTaxonomyValue; + } + | { + type: 'coverage_hole'; + hourStart: string; + }; + +export type CostInsightReconciliationReport = { + startHour: string; + endHourExclusive: string; + checkedHourCount: number; + mismatchCount: number; + mismatchCounts: Record; + mismatches: CostInsightReconciliationMismatch[]; + detailsTruncated: boolean; +}; + +type RollupAggregateRow = { + hour_start: string | Date; + owned_by_user_id: string | null; + owned_by_organization_id: string | null; + spend_category: CostInsightSpendCategory; + total_microdollars: string | number | bigint; + spend_record_count: string | number | bigint; +}; + +type VerificationRow = { + mismatch_count: string | number | bigint; +}; + +function normalizeHourRange(params: { + startHour: string; + endHourExclusive: string; + maxHours: number; +}): { startHour: string; endHourExclusive: string; hourCount: number } { + if (!Number.isSafeInteger(params.maxHours) || params.maxHours <= 0) { + throw new Error('Cost Insights maxHours must be an explicit positive safe integer.'); + } + const startHour = requireUtcHour(params.startHour, 'startHour'); + const endHourExclusive = requireUtcHour(params.endHourExclusive, 'endHourExclusive'); + const hourCount = (Date.parse(endHourExclusive) - Date.parse(startHour)) / HOUR_MS; + if (!Number.isInteger(hourCount) || hourCount <= 0 || hourCount > params.maxHours) { + throw new Error(`Cost Insights maintenance range must contain 1-${params.maxHours} UTC hours.`); + } + return { startHour, endHourExclusive, hourCount }; +} + +async function setCostInsightMaintenanceTimeouts( + transaction: Pick +): Promise { + await transaction.execute( + sql.raw(`SET LOCAL statement_timeout = '${MAINTENANCE_STATEMENT_TIMEOUT_MS}'`) + ); + await transaction.execute(sql.raw(`SET LOCAL lock_timeout = '${MAINTENANCE_LOCK_TIMEOUT_MS}'`)); +} + +function nextHour(hourStart: string): string { + return new Date(Date.parse(hourStart) + HOUR_MS).toISOString(); +} + +function ownerIdentity(owner: CostInsightSpendOwner): string { + return `${owner.type}:${owner.id}`; +} + +function aggregateIdentity( + owner: CostInsightSpendOwner, + category: CostInsightSpendCategory +): string { + return `${ownerIdentity(owner)}:${category}`; +} + +function ownerFromColumns( + ownedByUserId: string | null, + ownedByOrganizationId: string | null +): CostInsightSpendOwner { + if (ownedByOrganizationId && !ownedByUserId) { + return { type: 'organization', id: ownedByOrganizationId }; + } + if (ownedByUserId && !ownedByOrganizationId) { + return { type: 'user', id: ownedByUserId }; + } + throw new Error('Cost Insights rollup row must have exactly one Spend owner.'); +} + +function ownerColumns(owner: CostInsightSpendOwner): { + owned_by_user_id: string | null; + owned_by_organization_id: string | null; +} { + return owner.type === 'organization' + ? { owned_by_user_id: null, owned_by_organization_id: owner.id } + : { owned_by_user_id: owner.id, owned_by_organization_id: null }; +} + +function ownerSqlPredicate( + owner: CostInsightSpendOwner, + ownedByUserColumn: SQL, + ownedByOrganizationColumn: SQL +): SQL { + return owner.type === 'organization' + ? sql`${ownedByUserColumn} IS NULL AND ${ownedByOrganizationColumn} = ${owner.id}` + : sql`${ownedByOrganizationColumn} IS NULL AND ${ownedByUserColumn} = ${owner.id}`; +} + +function ownerTotalsWhere(owner: CostInsightSpendOwner, hourStart: string): SQL { + return sql`${cost_insight_owner_hour_totals.hour_start} = ${hourStart} + AND ${ownerSqlPredicate( + owner, + sql`${cost_insight_owner_hour_totals.owned_by_user_id}`, + sql`${cost_insight_owner_hour_totals.owned_by_organization_id}` + )}`; +} + +function ownerDriversWhere(owner: CostInsightSpendOwner, hourStart: string): SQL { + return sql`${cost_insight_owner_hour_driver_buckets.hour_start} = ${hourStart} + AND ${ownerSqlPredicate( + owner, + sql`${cost_insight_owner_hour_driver_buckets.owned_by_user_id}`, + sql`${cost_insight_owner_hour_driver_buckets.owned_by_organization_id}` + )}`; +} + +function sumSafe(values: number[], fieldName: string): number { + return values.reduce((total, value) => { + const result = total + value; + if (!Number.isSafeInteger(result)) { + throw new Error(`${fieldName} is outside the JavaScript safe-integer range.`); + } + return result; + }, 0); +} + +function chunkRows(rows: T[]): T[][] { + const chunks: T[][] = []; + for (let index = 0; index < rows.length; index += INSERT_BATCH_SIZE) { + chunks.push(rows.slice(index, index + INSERT_BATCH_SIZE)); + } + return chunks; +} + +export function assertCanonicalTotalsMatchDrivers( + aggregation: Pick +): void { + const driverSums = new Map(); + for (const driver of aggregation.drivers) { + const key = aggregateIdentity(driver.owner, driver.category); + const prior = driverSums.get(key) ?? { amount: 0, count: 0 }; + driverSums.set(key, { + amount: sumSafe( + [prior.amount, driver.totalMicrodollars], + 'canonical driver sum microdollars' + ), + count: sumSafe([prior.count, driver.spendRecordCount], 'canonical driver sum record count'), + }); + } + for (const total of aggregation.totals) { + const driverSum = driverSums.get(aggregateIdentity(total.owner, total.category)); + if ( + !driverSum || + driverSum.amount !== total.totalMicrodollars || + driverSum.count !== total.spendRecordCount + ) { + throw new Error('Canonical Cost Insights totals do not equal combined driver sums.'); + } + driverSums.delete(aggregateIdentity(total.owner, total.category)); + } + if (driverSums.size > 0) { + throw new Error('Canonical Cost Insights drivers exist without a matching owner total.'); + } +} + +async function insertTotals( + transaction: DrizzleTransaction, + hourStart: string, + totals: CanonicalCostInsightOwnerTotal[] +): Promise { + for (const chunk of chunkRows(totals)) { + await transaction.insert(cost_insight_owner_hour_totals).values( + chunk.map(total => ({ + ...ownerColumns(total.owner), + hour_start: hourStart, + spend_category: total.category, + total_microdollars: total.totalMicrodollars, + spend_record_count: total.spendRecordCount, + })) + ); + } +} + +async function insertDrivers( + transaction: DrizzleTransaction, + hourStart: string, + drivers: CanonicalCostInsightDriverAggregate[] +): Promise { + for (const chunk of chunkRows(drivers)) { + await transaction.insert(cost_insight_owner_hour_driver_buckets).values( + chunk.map(driver => ({ + ...ownerColumns(driver.owner), + hour_start: hourStart, + spend_category: driver.category, + driver_key: driver.driverKey, + source: driver.source, + product_key: driver.productKey, + feature_key: driver.featureKey, + model_or_plan_key: driver.modelOrPlanKey, + provider_key: driver.providerKey, + actor_user_id: driver.actorUserId, + total_microdollars: driver.totalMicrodollars, + spend_record_count: driver.spendRecordCount, + })) + ); + } +} + +async function verifyPersistedTotalsMatchDrivers( + transaction: DrizzleTransaction, + params: { hourStart: string; owner?: CostInsightSpendOwner } +): Promise { + const totalOwnerPredicate = params.owner + ? ownerSqlPredicate(params.owner, sql`t.owned_by_user_id`, sql`t.owned_by_organization_id`) + : sql`TRUE`; + const driverOwnerPredicate = params.owner + ? ownerSqlPredicate(params.owner, sql`d.owned_by_user_id`, sql`d.owned_by_organization_id`) + : sql`TRUE`; + const result = await transaction.execute(sql` + WITH totals AS ( + SELECT + owned_by_user_id, + owned_by_organization_id, + spend_category, + total_microdollars, + spend_record_count + FROM ${cost_insight_owner_hour_totals} t + WHERE t.hour_start = ${params.hourStart} + AND ${totalOwnerPredicate} + ), driver_sums AS ( + SELECT + owned_by_user_id, + owned_by_organization_id, + spend_category, + SUM(total_microdollars) AS total_microdollars, + SUM(spend_record_count) AS spend_record_count + FROM ${cost_insight_owner_hour_driver_buckets} d + WHERE d.hour_start = ${params.hourStart} + AND ${driverOwnerPredicate} + GROUP BY 1, 2, 3 + ) + SELECT COUNT(*)::text AS mismatch_count + FROM totals + FULL OUTER JOIN driver_sums + ON totals.owned_by_user_id IS NOT DISTINCT FROM driver_sums.owned_by_user_id + AND totals.owned_by_organization_id IS NOT DISTINCT FROM driver_sums.owned_by_organization_id + AND totals.spend_category = driver_sums.spend_category + WHERE totals.total_microdollars IS DISTINCT FROM driver_sums.total_microdollars + OR totals.spend_record_count IS DISTINCT FROM driver_sums.spend_record_count + `); + const mismatchCount = parseSafeDatabaseInteger( + result.rows[0]?.mismatch_count ?? 0, + 'persisted total/driver mismatch count' + ); + if (mismatchCount !== 0) { + throw new Error('Persisted Cost Insights totals do not equal combined driver sums.'); + } +} + +async function replaceAllRollupsForHour( + transaction: DrizzleTransaction, + hourStart: string, + aggregation: CanonicalCostInsightAggregation +): Promise { + assertCanonicalTotalsMatchDrivers(aggregation); + await transaction + .delete(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.hour_start, hourStart)); + await transaction + .delete(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.hour_start, hourStart)); + await insertTotals(transaction, hourStart, aggregation.totals); + await insertDrivers(transaction, hourStart, aggregation.drivers); + await verifyPersistedTotalsMatchDrivers(transaction, { hourStart }); +} + +async function replaceOwnerRollupsForHour( + transaction: DrizzleTransaction, + owner: CostInsightSpendOwner, + hourStart: string, + aggregation: CanonicalCostInsightAggregation +): Promise { + assertCanonicalTotalsMatchDrivers(aggregation); + await transaction + .delete(cost_insight_owner_hour_driver_buckets) + .where(ownerDriversWhere(owner, hourStart)); + await transaction + .delete(cost_insight_owner_hour_totals) + .where(ownerTotalsWhere(owner, hourStart)); + await insertTotals(transaction, hourStart, aggregation.totals); + await insertDrivers(transaction, hourStart, aggregation.drivers); + await verifyPersistedTotalsMatchDrivers(transaction, { hourStart, owner }); +} + +async function advanceCoverageIfContiguous( + transaction: DrizzleTransaction, + hourStart: string, + endHourExclusive: string +): Promise { + const updated = await transaction + .update(cost_insight_rollup_coverage) + .set({ coverage_start_hour: hourStart, updated_at: sql`now()` }) + .where( + and( + eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION), + or( + eq(cost_insight_rollup_coverage.coverage_start_hour, endHourExclusive), + and( + isNull(cost_insight_rollup_coverage.coverage_start_hour), + eq(cost_insight_rollup_coverage.live_capture_start_hour, endHourExclusive) + ) + ) + ) + ); + return (updated.rowCount ?? 0) > 0; +} + +function summarizeReplacement( + hourStart: string, + aggregation: CanonicalCostInsightAggregation, + coverageAdvanced: boolean, + startedAt: number +): CostInsightHourReplacementResult { + return { + hourStart, + totalRowCount: aggregation.totals.length, + driverRowCount: aggregation.drivers.length, + canonicalSpendRecordCount: sumSafe( + aggregation.totals.map(total => total.spendRecordCount), + 'canonical hour record count' + ), + canonicalMicrodollars: sumSafe( + aggregation.totals.map(total => total.totalMicrodollars), + 'canonical hour microdollars' + ), + coverageAdvanced, + durationMs: performance.now() - startedAt, + }; +} + +export async function initializeCostInsightRollupCoverage( + database: CostInsightMaintenanceDatabase, + liveCaptureStartHourInput: string +): Promise { + const liveCaptureStartHour = requireUtcHour(liveCaptureStartHourInput, 'liveCaptureStartHour'); + + await database.transaction( + async transaction => { + const [inserted] = await transaction + .insert(cost_insight_rollup_coverage) + .values({ + rollup_version: COST_INSIGHT_ROLLUP_VERSION, + live_capture_start_hour: liveCaptureStartHour, + coverage_start_hour: liveCaptureStartHour, + }) + .onConflictDoNothing() + .returning({ rollupVersion: cost_insight_rollup_coverage.rollup_version }); + if (inserted) return; + + const [existing] = await transaction + .select({ + liveCaptureStartHour: cost_insight_rollup_coverage.live_capture_start_hour, + coverageStartHour: cost_insight_rollup_coverage.coverage_start_hour, + }) + .from(cost_insight_rollup_coverage) + .where(eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION)) + .limit(1) + .for('update'); + if (!existing) { + throw new Error('Cost Insights coverage initialization lost its version row.'); + } + if ( + existing.liveCaptureStartHour !== null && + new Date(existing.liveCaptureStartHour).toISOString() === liveCaptureStartHour + ) { + return; + } + if (existing.liveCaptureStartHour === null && existing.coverageStartHour === null) { + await transaction + .update(cost_insight_rollup_coverage) + .set({ + live_capture_start_hour: liveCaptureStartHour, + coverage_start_hour: liveCaptureStartHour, + updated_at: sql`now()`, + }) + .where(eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION)); + return; + } + throw new Error( + `Cost Insights live capture start is already initialized as ${String(existing.liveCaptureStartHour)}.` + ); + }, + { isolationLevel: 'read committed' } + ); +} + +export async function recordCostInsightDegradedInterval( + database: CostInsightMaintenanceDatabase, + params: { + startHour: string; + endHourExclusive: string; + source?: CostInsightSpendSource; + reason: CostInsightRollupDegradedReason; + } +): Promise { + const range = normalizeHourRange({ ...params, maxHours: Number.MAX_SAFE_INTEGER }); + return database.transaction( + async transaction => { + await transaction.execute( + sql`SELECT pg_catalog.pg_advisory_xact_lock( + pg_catalog.hashtextextended('cost-insight-degraded-intervals:v1', 0::bigint) + )` + ); + const overlapping = await transaction + .select({ + id: cost_insight_rollup_degraded_intervals.id, + startHour: cost_insight_rollup_degraded_intervals.start_hour, + endHourExclusive: cost_insight_rollup_degraded_intervals.end_hour_exclusive, + }) + .from(cost_insight_rollup_degraded_intervals) + .where( + and( + isNull(cost_insight_rollup_degraded_intervals.resolved_at), + eq(cost_insight_rollup_degraded_intervals.reason, params.reason), + lte(cost_insight_rollup_degraded_intervals.start_hour, range.endHourExclusive), + gte(cost_insight_rollup_degraded_intervals.end_hour_exclusive, range.startHour), + sql`${cost_insight_rollup_degraded_intervals.source} IS NOT DISTINCT FROM ${params.source ?? null}` + ) + ) + .orderBy( + asc(cost_insight_rollup_degraded_intervals.start_hour), + asc(cost_insight_rollup_degraded_intervals.id) + ) + .for('update'); + + const [keeper, ...duplicates] = overlapping; + if (keeper) { + const mergedStartHour = [range.startHour, ...overlapping.map(row => row.startHour)] + .map(value => new Date(value).toISOString()) + .sort()[0]; + const mergedEndHourExclusive = [ + range.endHourExclusive, + ...overlapping.map(row => row.endHourExclusive), + ] + .map(value => new Date(value).toISOString()) + .sort() + .at(-1); + if (!mergedStartHour || !mergedEndHourExclusive) { + throw new Error('Cost Insights degraded interval merge produced an empty range.'); + } + await transaction + .update(cost_insight_rollup_degraded_intervals) + .set({ + start_hour: mergedStartHour, + end_hour_exclusive: mergedEndHourExclusive, + updated_at: sql`now()`, + }) + .where(eq(cost_insight_rollup_degraded_intervals.id, keeper.id)); + if (duplicates.length > 0) { + await transaction.delete(cost_insight_rollup_degraded_intervals).where( + inArray( + cost_insight_rollup_degraded_intervals.id, + duplicates.map(interval => interval.id) + ) + ); + } + return keeper.id; + } + + const [inserted] = await transaction + .insert(cost_insight_rollup_degraded_intervals) + .values({ + start_hour: range.startHour, + end_hour_exclusive: range.endHourExclusive, + source: params.source, + reason: params.reason, + }) + .returning({ id: cost_insight_rollup_degraded_intervals.id }); + if (!inserted) throw new Error('Cost Insights degraded interval insert returned no row.'); + return inserted.id; + }, + { isolationLevel: 'read committed' } + ); +} + +export async function resolveCostInsightDegradedInterval( + database: CostInsightMaintenanceDatabase, + intervalId: string +): Promise { + if (!intervalId) throw new Error('Cost Insights degraded interval ID is required.'); + await database.transaction( + async transaction => { + const updated = await transaction + .update(cost_insight_rollup_degraded_intervals) + .set({ resolved_at: sql`now()`, updated_at: sql`now()` }) + .where( + and( + eq(cost_insight_rollup_degraded_intervals.id, intervalId), + isNull(cost_insight_rollup_degraded_intervals.resolved_at) + ) + ); + if ((updated.rowCount ?? 0) !== 1) { + throw new Error('Cost Insights degraded interval is missing or already resolved.'); + } + }, + { isolationLevel: 'read committed' } + ); +} + +async function assertBulkBackfillPrecedesLiveCapture( + transaction: DrizzleTransaction, + endHourExclusive: string +): Promise { + const [coverage] = await transaction + .select({ liveCaptureStartHour: cost_insight_rollup_coverage.live_capture_start_hour }) + .from(cost_insight_rollup_coverage) + .where(eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION)) + .limit(1); + if (!coverage?.liveCaptureStartHour) { + throw new Error('Cost Insights live capture start must be initialized before bulk backfill.'); + } + if (Date.parse(endHourExclusive) > Date.parse(coverage.liveCaptureStartHour)) { + throw new Error( + 'Cost Insights bulk backfill is restricted to pre-cutover hours; use owner repair for live-capture intervals.' + ); + } +} + +export async function backfillCostInsightHour( + database: CostInsightMaintenanceDatabase, + hourStartInput: string +): Promise { + const hourStart = requireUtcHour(hourStartInput, 'hourStart'); + const endHourExclusive = nextHour(hourStart); + const startedAt = performance.now(); + return database.transaction( + async transaction => { + await setCostInsightMaintenanceTimeouts(transaction); + await assertBulkBackfillPrecedesLiveCapture(transaction, endHourExclusive); + const aggregation = await loadCanonicalCostInsightAggregation(transaction, { + startInclusive: hourStart, + endExclusive: endHourExclusive, + }); + await replaceAllRollupsForHour(transaction, hourStart, aggregation); + const coverageAdvanced = await advanceCoverageIfContiguous( + transaction, + hourStart, + endHourExclusive + ); + return summarizeReplacement(hourStart, aggregation, coverageAdvanced, startedAt); + }, + { isolationLevel: 'repeatable read' } + ); +} + +async function persistBackfilledCostInsightHour( + database: CostInsightMaintenanceDatabase, + hourStart: string, + aggregation: CanonicalCostInsightAggregation, + startedAt: number +): Promise { + const endHourExclusive = nextHour(hourStart); + return database.transaction( + async transaction => { + await setCostInsightMaintenanceTimeouts(transaction); + await assertBulkBackfillPrecedesLiveCapture(transaction, endHourExclusive); + await replaceAllRollupsForHour(transaction, hourStart, aggregation); + const coverageAdvanced = await advanceCoverageIfContiguous( + transaction, + hourStart, + endHourExclusive + ); + return summarizeReplacement(hourStart, aggregation, coverageAdvanced, startedAt); + }, + { isolationLevel: 'read committed' } + ); +} + +export async function backfillCostInsightRollupsNewestFirst( + database: CostInsightMaintenanceDatabase, + params: { + startHour: string; + endHourExclusive: string; + maxHours: number; + sleepMs?: number; + onHourComplete?: (result: CostInsightHourReplacementResult) => void | Promise; + } +): Promise { + const range = normalizeHourRange(params); + const sleepMs = params.sleepMs ?? 0; + if (!Number.isSafeInteger(sleepMs) || sleepMs < 0) { + throw new Error('Cost Insights sleepMs must be a non-negative safe integer.'); + } + const results: CostInsightHourReplacementResult[] = []; + let chunkEndTimestamp = Date.parse(range.endHourExclusive); + while (chunkEndTimestamp > Date.parse(range.startHour)) { + const chunkStartTimestamp = Math.max( + Date.parse(range.startHour), + chunkEndTimestamp - BACKFILL_SOURCE_CHUNK_HOURS * HOUR_MS + ); + const chunkStart = new Date(chunkStartTimestamp).toISOString(); + const chunkEnd = new Date(chunkEndTimestamp).toISOString(); + const hourly = await database.transaction( + async transaction => { + await setCostInsightMaintenanceTimeouts(transaction); + await assertBulkBackfillPrecedesLiveCapture(transaction, chunkEnd); + return loadCanonicalCostInsightAggregationsByHour(transaction, { + startInclusive: chunkStart, + endExclusive: chunkEnd, + }); + }, + { isolationLevel: 'repeatable read', accessMode: 'read only' } + ); + const canonicalByHour = new Map( + hourly.map(aggregation => [aggregation.hourStart, aggregation]) + ); + + for ( + let hourTimestamp = chunkEndTimestamp - HOUR_MS; + hourTimestamp >= chunkStartTimestamp; + hourTimestamp -= HOUR_MS + ) { + const hourStart = new Date(hourTimestamp).toISOString(); + const startedAt = performance.now(); + const aggregation = canonicalByHour.get(hourStart) ?? { + totals: [], + drivers: [], + unknownTaxonomyValues: [], + }; + const result = await persistBackfilledCostInsightHour( + database, + hourStart, + aggregation, + startedAt + ); + results.push(result); + await params.onHourComplete?.(result); + if (sleepMs > 0 && hourTimestamp > Date.parse(range.startHour)) { + await new Promise(resolve => setTimeout(resolve, sleepMs)); + } + } + chunkEndTimestamp = chunkStartTimestamp; + } + return results; +} + +export async function repairOwnerSpendRollups( + database: CostInsightMaintenanceDatabase, + params: { + owner: CostInsightSpendOwner; + startHour: string; + endHourExclusive: string; + maxHours: number; + } +): Promise { + const range = normalizeHourRange(params); + const results: CostInsightHourReplacementResult[] = []; + for ( + let hourTimestamp = Date.parse(range.startHour); + hourTimestamp < Date.parse(range.endHourExclusive); + hourTimestamp += HOUR_MS + ) { + const hourStart = new Date(hourTimestamp).toISOString(); + const endHourExclusive = nextHour(hourStart); + const startedAt = performance.now(); + const result = await database.transaction( + async transaction => { + await setCostInsightMaintenanceTimeouts(transaction); + await acquireCostInsightOwnerHourLock(transaction, params.owner, hourStart); + const aggregation = await loadCanonicalCostInsightAggregation(transaction, { + owner: params.owner, + startInclusive: hourStart, + endExclusive: endHourExclusive, + }); + await replaceOwnerRollupsForHour(transaction, params.owner, hourStart, aggregation); + return summarizeReplacement(hourStart, aggregation, false, startedAt); + }, + { isolationLevel: 'read committed' } + ); + results.push(result); + } + return results; +} + +function aggregateRowMap(rows: RollupAggregateRow[]): Map< + string, + { + owner: CostInsightSpendOwner; + category: CostInsightSpendCategory; + totalMicrodollars: number; + spendRecordCount: number; + } +> { + return new Map( + rows.map(row => { + const owner = ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id); + const value = { + owner, + category: row.spend_category, + totalMicrodollars: parseSafeDatabaseInteger( + row.total_microdollars, + 'reconciliation total_microdollars' + ), + spendRecordCount: parseSafeDatabaseInteger( + row.spend_record_count, + 'reconciliation spend_record_count' + ), + }; + return [aggregateIdentity(owner, row.spend_category), value]; + }) + ); +} + +type PersistedHourAggregates = { + totals: ReturnType; + driverSums: ReturnType; +}; + +function groupRollupRowsByHour(rows: RollupAggregateRow[]): Map { + const grouped = new Map(); + for (const row of rows) { + const hourStart = new Date(row.hour_start).toISOString(); + const existing = grouped.get(hourStart); + if (existing) { + existing.push(row); + } else { + grouped.set(hourStart, [row]); + } + } + return grouped; +} + +async function loadPersistedHourlyAggregates( + transaction: DrizzleTransaction, + range: { startHour: string; endHourExclusive: string } +): Promise> { + const totalsResult = await transaction.execute(sql` + SELECT + hour_start, + owned_by_user_id, + owned_by_organization_id, + spend_category, + total_microdollars::text AS total_microdollars, + spend_record_count::text AS spend_record_count + FROM ${cost_insight_owner_hour_totals} + WHERE hour_start >= ${range.startHour} + AND hour_start < ${range.endHourExclusive} + `); + const driverResult = await transaction.execute(sql` + SELECT + hour_start, + owned_by_user_id, + owned_by_organization_id, + spend_category, + SUM(total_microdollars)::text AS total_microdollars, + SUM(spend_record_count)::text AS spend_record_count + FROM ${cost_insight_owner_hour_driver_buckets} + WHERE hour_start >= ${range.startHour} + AND hour_start < ${range.endHourExclusive} + GROUP BY 1, 2, 3, 4 + `); + const totalsByHour = groupRollupRowsByHour(totalsResult.rows); + const driversByHour = groupRollupRowsByHour(driverResult.rows); + const hourly = new Map(); + for (const hourStart of new Set([...totalsByHour.keys(), ...driversByHour.keys()])) { + hourly.set(hourStart, { + totals: aggregateRowMap(totalsByHour.get(hourStart) ?? []), + driverSums: aggregateRowMap(driversByHour.get(hourStart) ?? []), + }); + } + return hourly; +} + +export function compareCostInsightHourAggregates(params: { + hourStart: string; + canonicalTotals: CanonicalCostInsightOwnerTotal[]; + persistedTotals: ReturnType; + persistedDriverSums: ReturnType; + unknownTaxonomyValues?: CostInsightUnknownTaxonomyValue[]; +}): CostInsightReconciliationMismatch[] { + const mismatches: CostInsightReconciliationMismatch[] = []; + const canonical = new Map( + params.canonicalTotals.map(total => [aggregateIdentity(total.owner, total.category), total]) + ); + const keys = new Set([ + ...canonical.keys(), + ...params.persistedTotals.keys(), + ...params.persistedDriverSums.keys(), + ]); + + for (const key of [...keys].sort()) { + const expected = canonical.get(key); + const actual = params.persistedTotals.get(key); + const driverSum = params.persistedDriverSums.get(key); + const owner = expected?.owner ?? actual?.owner ?? driverSum?.owner; + const category = expected?.category ?? actual?.category ?? driverSum?.category; + if (!owner || !category) { + throw new Error('Reconciliation aggregate identity has no owner/category payload.'); + } + if (expected && !actual) { + mismatches.push({ + type: 'missing_total', + hourStart: params.hourStart, + owner, + category, + expectedMicrodollars: expected.totalMicrodollars, + expectedRecordCount: expected.spendRecordCount, + }); + } else if ((expected?.totalMicrodollars ?? 0) !== (actual?.totalMicrodollars ?? 0)) { + mismatches.push({ + type: 'amount_difference', + hourStart: params.hourStart, + owner, + category, + expectedMicrodollars: expected?.totalMicrodollars ?? 0, + actualMicrodollars: actual?.totalMicrodollars ?? 0, + }); + } + if ((expected?.spendRecordCount ?? 0) !== (actual?.spendRecordCount ?? 0)) { + mismatches.push({ + type: 'record_count_difference', + hourStart: params.hourStart, + owner, + category, + expectedRecordCount: expected?.spendRecordCount ?? 0, + actualRecordCount: actual?.spendRecordCount ?? 0, + }); + } + if ( + (expected?.totalMicrodollars ?? 0) !== (driverSum?.totalMicrodollars ?? 0) || + (expected?.spendRecordCount ?? 0) !== (driverSum?.spendRecordCount ?? 0) + ) { + mismatches.push({ + type: 'driver_sum_difference', + hourStart: params.hourStart, + owner, + category, + expectedMicrodollars: expected?.totalMicrodollars ?? 0, + actualMicrodollars: driverSum?.totalMicrodollars ?? 0, + expectedRecordCount: expected?.spendRecordCount ?? 0, + actualRecordCount: driverSum?.spendRecordCount ?? 0, + }); + } + } + for (const value of params.unknownTaxonomyValues ?? []) { + mismatches.push({ type: 'unknown_taxonomy_value', hourStart: params.hourStart, value }); + } + return mismatches; +} + +function hourIsCovered( + hourStart: string, + coverage: Awaited> +): boolean { + const effectiveStart = coverage.coverageStartHour ?? coverage.liveCaptureStartHour; + if (!effectiveStart || Date.parse(hourStart) < Date.parse(effectiveStart)) { + return false; + } + const hourEnd = Date.parse(hourStart) + HOUR_MS; + return !coverage.degradedIntervals.some( + interval => + Date.parse(interval.startHour) < hourEnd && + Date.parse(interval.endHourExclusive) > Date.parse(hourStart) + ); +} + +export async function recordCostInsightReconciliationSuccess( + database: CostInsightMaintenanceDatabase +): Promise { + await database.transaction( + async transaction => { + const updated = await transaction + .update(cost_insight_rollup_coverage) + .set({ last_reconciled_at: sql`now()`, updated_at: sql`now()` }) + .where(eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION)); + if ((updated.rowCount ?? 0) !== 1) { + throw new Error('Cost Insights coverage row is not initialized for reconciliation.'); + } + }, + { isolationLevel: 'read committed' } + ); +} + +function emptyMismatchCounts(): CostInsightReconciliationReport['mismatchCounts'] { + return { + missing_total: 0, + amount_difference: 0, + record_count_difference: 0, + driver_sum_difference: 0, + unknown_taxonomy_value: 0, + coverage_hole: 0, + }; +} + +async function reconcileCostInsightRollupChunk( + transaction: DrizzleTransaction, + params: { + startHour: string; + endHourExclusive: string; + maxMismatchDetails: number; + } +): Promise { + await setCostInsightMaintenanceTimeouts(transaction); + const coverage = await getCostInsightRollupCoverage(transaction, params); + const canonicalByHour = new Map( + ( + await loadCanonicalCostInsightAggregationsByHour(transaction, { + startInclusive: params.startHour, + endExclusive: params.endHourExclusive, + }) + ).map(aggregation => [aggregation.hourStart, aggregation]) + ); + const persistedByHour = await loadPersistedHourlyAggregates(transaction, params); + const mismatchCounts = emptyMismatchCounts(); + const mismatches: CostInsightReconciliationMismatch[] = []; + let mismatchCount = 0; + const appendMismatch = (mismatch: CostInsightReconciliationMismatch) => { + mismatchCount++; + mismatchCounts[mismatch.type]++; + if (mismatches.length < params.maxMismatchDetails) { + mismatches.push(mismatch); + } + }; + + for ( + let hourTimestamp = Date.parse(params.startHour); + hourTimestamp < Date.parse(params.endHourExclusive); + hourTimestamp += HOUR_MS + ) { + const hourStart = new Date(hourTimestamp).toISOString(); + const canonical = canonicalByHour.get(hourStart); + const persisted = persistedByHour.get(hourStart) ?? { + totals: aggregateRowMap([]), + driverSums: aggregateRowMap([]), + }; + for (const mismatch of compareCostInsightHourAggregates({ + hourStart, + canonicalTotals: canonical?.totals ?? [], + persistedTotals: persisted.totals, + persistedDriverSums: persisted.driverSums, + unknownTaxonomyValues: canonical?.unknownTaxonomyValues, + })) { + appendMismatch(mismatch); + } + if (!hourIsCovered(hourStart, coverage)) { + appendMismatch({ type: 'coverage_hole', hourStart }); + } + } + + return { + startHour: params.startHour, + endHourExclusive: params.endHourExclusive, + checkedHourCount: + (Date.parse(params.endHourExclusive) - Date.parse(params.startHour)) / HOUR_MS, + mismatchCount, + mismatchCounts, + mismatches, + detailsTruncated: mismatchCount > mismatches.length, + }; +} + +export async function reconcileCostInsightRollups( + database: CostInsightMaintenanceDatabase, + params: { + startHour: string; + endHourExclusive: string; + maxHours: number; + maxMismatchDetails?: number; + chunkHours?: number; + } +): Promise { + const range = normalizeHourRange(params); + const maxMismatchDetails = params.maxMismatchDetails ?? MAX_RECONCILIATION_MISMATCH_DETAILS; + if (!Number.isSafeInteger(maxMismatchDetails) || maxMismatchDetails < 0) { + throw new Error('maxMismatchDetails must be a non-negative safe integer.'); + } + const chunkHours = params.chunkHours ?? DEFAULT_RECONCILIATION_CHUNK_HOURS; + if ( + !Number.isSafeInteger(chunkHours) || + chunkHours <= 0 || + chunkHours > MAX_RECONCILIATION_CHUNK_HOURS + ) { + throw new Error( + `chunkHours must be a positive safe integer no greater than ${MAX_RECONCILIATION_CHUNK_HOURS}.` + ); + } + + const mismatchCounts = emptyMismatchCounts(); + const mismatches: CostInsightReconciliationMismatch[] = []; + let mismatchCount = 0; + for ( + let chunkStartTimestamp = Date.parse(range.startHour); + chunkStartTimestamp < Date.parse(range.endHourExclusive); + chunkStartTimestamp += chunkHours * HOUR_MS + ) { + const chunkStart = new Date(chunkStartTimestamp).toISOString(); + const chunkEnd = new Date( + Math.min(chunkStartTimestamp + chunkHours * HOUR_MS, Date.parse(range.endHourExclusive)) + ).toISOString(); + const chunk = await database.transaction( + transaction => + reconcileCostInsightRollupChunk(transaction, { + startHour: chunkStart, + endHourExclusive: chunkEnd, + maxMismatchDetails: Math.max(0, maxMismatchDetails - mismatches.length), + }), + { isolationLevel: 'repeatable read', accessMode: 'read only' } + ); + mismatchCount = sumSafe([mismatchCount, chunk.mismatchCount], 'reconciliation mismatch count'); + for (const type of Object.keys(mismatchCounts) as Array) { + mismatchCounts[type] = sumSafe( + [mismatchCounts[type], chunk.mismatchCounts[type]], + `reconciliation ${type} mismatch count` + ); + } + mismatches.push(...chunk.mismatches); + } + + return { + startHour: range.startHour, + endHourExclusive: range.endHourExclusive, + checkedHourCount: range.hourCount, + mismatchCount, + mismatchCounts, + mismatches, + detailsTruncated: mismatchCount > mismatches.length, + }; +} diff --git a/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts b/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts new file mode 100644 index 0000000000..565eedd0f8 --- /dev/null +++ b/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts @@ -0,0 +1,199 @@ +import { afterEach, describe, expect, test } from '@jest/globals'; +import { eq } from 'drizzle-orm'; + +import { db } from '@/lib/drizzle'; +import { + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, + cost_insight_rollup_coverage, + cost_insight_rollup_degraded_intervals, + kilocode_users, + microdollar_usage, +} from '@kilocode/db/schema'; + +import { + getOwnerHourlySpend, + getOwnerRolling24HourSpendExact, + getOwnerTopSpendDrivers, +} from './spend-repository'; + +const testUserIds = new Set(); + +async function createUser(): Promise { + const id = `cost-insights-read-${crypto.randomUUID()}`; + testUserIds.add(id); + await db.insert(kilocode_users).values({ + id, + google_user_email: `${id}@example.com`, + google_user_name: 'Cost Insights Read Test', + google_user_image_url: 'https://example.com/avatar.png', + stripe_customer_id: `cus_${crypto.randomUUID()}`, + }); + return id; +} + +async function initializeCoverage(): Promise { + await db.insert(cost_insight_rollup_coverage).values({ + rollup_version: 1, + live_capture_start_hour: '2026-06-01T00:00:00.000Z', + coverage_start_hour: '2026-05-01T00:00:00.000Z', + }); +} + +afterEach(async () => { + await db + .delete(cost_insight_rollup_degraded_intervals) + .where(eq(cost_insight_rollup_degraded_intervals.reason, 'capture_bypass')); + await db + .delete(cost_insight_rollup_coverage) + .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); + for (const userId of testUserIds) { + await db + .delete(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); + await db + .delete(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); + await db.delete(microdollar_usage).where(eq(microdollar_usage.kilo_user_id, userId)); + await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); + } + testUserIds.clear(); +}); + +describe('Cost Insights spend repository integration', () => { + test('zero-fills covered sparse hours, isolates owners, and suppresses degraded hours', async () => { + const userId = await createUser(); + const otherUserId = await createUser(); + await initializeCoverage(); + await db.insert(cost_insight_owner_hour_totals).values([ + { + owned_by_user_id: userId, + hour_start: '2026-06-01T00:00:00.000Z', + spend_category: 'variable', + total_microdollars: 10, + spend_record_count: 2, + }, + { + owned_by_user_id: otherUserId, + hour_start: '2026-06-01T00:00:00.000Z', + spend_category: 'scheduled', + total_microdollars: 999, + spend_record_count: 1, + }, + ]); + await db.insert(cost_insight_owner_hour_driver_buckets).values({ + owned_by_user_id: userId, + hour_start: '2026-06-01T00:00:00.000Z', + spend_category: 'variable', + driver_key: 'a'.repeat(64), + source: 'ai_gateway', + product_key: 'direct-gateway', + feature_key: 'chat_completions', + model_or_plan_key: 'model', + provider_key: 'provider', + actor_user_id: userId, + total_microdollars: 10, + spend_record_count: 2, + }); + + const owner = { type: 'user', id: userId } as const; + await expect( + getOwnerHourlySpend(db, { + owner, + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + }) + ).resolves.toEqual([ + expect.objectContaining({ + hourStart: '2026-06-01T00:00:00.000Z', + variableMicrodollars: 10, + scheduledMicrodollars: 0, + totalMicrodollars: 10, + isCovered: true, + }), + expect.objectContaining({ + hourStart: '2026-06-01T01:00:00.000Z', + variableMicrodollars: 0, + scheduledMicrodollars: 0, + totalMicrodollars: 0, + isCovered: true, + }), + ]); + await expect( + getOwnerTopSpendDrivers(db, { + owner, + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + }) + ).resolves.toEqual([ + expect.objectContaining({ + actorUserId: userId, + totalMicrodollars: 10, + spendRecordCount: 2, + }), + ]); + + await db.insert(cost_insight_rollup_degraded_intervals).values({ + start_hour: '2026-06-01T01:00:00.000Z', + end_hour_exclusive: '2026-06-01T02:00:00.000Z', + reason: 'capture_bypass', + }); + const degraded = await getOwnerHourlySpend(db, { + owner, + startHour: '2026-06-01T01:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + }); + expect(degraded[0]).toMatchObject({ + isCovered: false, + variableMicrodollars: null, + scheduledMicrodollars: null, + totalMicrodollars: null, + }); + }); + + test('combines rollup interior with canonical raw boundary fragments exactly once', async () => { + const userId = await createUser(); + await initializeCoverage(); + await db.insert(cost_insight_owner_hour_totals).values({ + owned_by_user_id: userId, + hour_start: '2026-06-01T13:00:00.000Z', + spend_category: 'variable', + total_microdollars: 100, + spend_record_count: 1, + }); + await db.insert(microdollar_usage).values([ + { + kilo_user_id: userId, + cost: 3, + input_tokens: 0, + output_tokens: 0, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-01T12:45:00.000Z', + }, + { + kilo_user_id: userId, + cost: 4, + input_tokens: 0, + output_tokens: 0, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-02T12:15:00.000Z', + }, + ]); + + await expect( + getOwnerRolling24HourSpendExact(db, { + owner: { type: 'user', id: userId }, + asOf: '2026-06-02T12:30:00.000Z', + }) + ).resolves.toEqual({ + asOf: '2026-06-02T12:30:00.000Z', + windowStart: '2026-06-01T12:30:00.000Z', + variableMicrodollars: 107, + scheduledMicrodollars: 0, + totalMicrodollars: 107, + isComplete: true, + }); + }); +}); diff --git a/apps/web/src/lib/cost-insights/spend-repository.test.ts b/apps/web/src/lib/cost-insights/spend-repository.test.ts new file mode 100644 index 0000000000..0ffeeade0a --- /dev/null +++ b/apps/web/src/lib/cost-insights/spend-repository.test.ts @@ -0,0 +1,160 @@ +import type { CostInsightQueryExecutor } from './canonical-sources'; +import { + getCostInsightRollupCoverage, + getOwnerCurrentHourSpend, + getOwnerHourlySpend, + getRolling24HourFragments, +} from './spend-repository'; + +const owner = { type: 'user', id: 'user-1' } as const; + +function executorReturning(...rowsByCall: unknown[][]): CostInsightQueryExecutor { + const execute = jest.fn(); + for (const rows of rowsByCall) { + execute.mockResolvedValueOnce({ rows }); + } + return { execute } as unknown as CostInsightQueryExecutor; +} + +describe('Cost Insights spend repository', () => { + test('splits exact rolling 24 hours into raw boundaries and rollup interior', () => { + expect(getRolling24HourFragments('2026-06-02T12:30:00.000Z')).toEqual({ + asOf: '2026-06-02T12:30:00.000Z', + windowStart: '2026-06-01T12:30:00.000Z', + oldestBoundaryEnd: '2026-06-01T13:00:00.000Z', + interiorStart: '2026-06-01T13:00:00.000Z', + interiorEnd: '2026-06-02T12:00:00.000Z', + currentBoundaryStart: '2026-06-02T12:00:00.000Z', + }); + }); + + test('skips both raw fragments on exact UTC-hour boundaries', () => { + const fragments = getRolling24HourFragments('2026-06-02T12:00:00.000Z'); + expect(fragments.windowStart).toBe(fragments.oldestBoundaryEnd); + expect(fragments.currentBoundaryStart).toBe(fragments.asOf); + expect(fragments.interiorStart).toBe('2026-06-01T12:00:00.000Z'); + expect(fragments.interiorEnd).toBe('2026-06-02T12:00:00.000Z'); + }); + + test('returns covered sparse hours as zero and uncovered hours as null', async () => { + const executor = executorReturning([ + { + hour_start: '2026-06-01 00:00:00+00', + variable_microdollars: '0', + scheduled_microdollars: '0', + variable_record_count: '0', + scheduled_record_count: '0', + is_covered: true, + }, + { + hour_start: '2026-06-01 01:00:00+00', + variable_microdollars: null, + scheduled_microdollars: null, + variable_record_count: null, + scheduled_record_count: null, + is_covered: false, + }, + ]); + + await expect( + getOwnerHourlySpend(executor, { + owner, + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + }) + ).resolves.toEqual([ + { + hourStart: '2026-06-01T00:00:00.000Z', + variableMicrodollars: 0, + scheduledMicrodollars: 0, + totalMicrodollars: 0, + variableRecordCount: 0, + scheduledRecordCount: 0, + isCovered: true, + }, + { + hourStart: '2026-06-01T01:00:00.000Z', + variableMicrodollars: null, + scheduledMicrodollars: null, + totalMicrodollars: null, + variableRecordCount: null, + scheduledRecordCount: null, + isCovered: false, + }, + ]); + }); + + test('adds current-hour categories using safe integer conversion', async () => { + const executor = executorReturning([ + { + variable_microdollars: '13', + scheduled_microdollars: '21', + variable_record_count: '2', + scheduled_record_count: '1', + }, + ]); + + await expect(getOwnerCurrentHourSpend(executor, owner)).resolves.toEqual({ + variableMicrodollars: 13, + scheduledMicrodollars: 21, + totalMicrodollars: 34, + variableRecordCount: 2, + scheduledRecordCount: 1, + }); + }); + + test('marks range incomplete when unresolved degraded interval overlaps it', async () => { + const executor = executorReturning( + [ + { + rollup_version: '1', + live_capture_start_hour: '2026-06-01 00:00:00+00', + coverage_start_hour: '2026-05-01 00:00:00+00', + last_reconciled_at: '2026-06-02 00:00:00+00', + database_now: '2026-06-03 00:30:00+00', + }, + ], + [ + { + id: 'degraded-1', + start_hour: '2026-06-01 01:00:00+00', + end_hour_exclusive: '2026-06-01 02:00:00+00', + source: 'other', + reason: 'capture_bypass', + detected_at: '2026-06-01 02:00:00+00', + }, + ] + ); + + const coverage = await getCostInsightRollupCoverage(executor, { + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T03:00:00.000Z', + }); + + expect(coverage).toMatchObject({ + rollupVersion: 1, + liveCaptureStartHour: '2026-06-01T00:00:00.000Z', + coverageStartHour: '2026-05-01T00:00:00.000Z', + lastReconciledAt: '2026-06-02T00:00:00.000Z', + isFullyCovered: false, + }); + expect(coverage.degradedIntervals).toEqual([ + expect.objectContaining({ + id: 'degraded-1', + startHour: '2026-06-01T01:00:00.000Z', + endHourExclusive: '2026-06-01T02:00:00.000Z', + }), + ]); + }); + + test('rejects more than 90 days of hourly buckets', async () => { + const executor = executorReturning([]); + await expect( + getOwnerHourlySpend(executor, { + owner, + startHour: '2026-01-01T00:00:00.000Z', + endHourExclusive: '2026-04-02T00:00:00.000Z', + }) + ).rejects.toThrow('between 1 and 2160 UTC hours'); + }); +}); diff --git a/apps/web/src/lib/cost-insights/spend-repository.ts b/apps/web/src/lib/cost-insights/spend-repository.ts new file mode 100644 index 0000000000..2b7a824243 --- /dev/null +++ b/apps/web/src/lib/cost-insights/spend-repository.ts @@ -0,0 +1,696 @@ +import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; +import { + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, + cost_insight_rollup_coverage, + cost_insight_rollup_degraded_intervals, +} from '@kilocode/db/schema'; +import { sql, type SQL } from 'drizzle-orm'; + +import type { db } from '@/lib/drizzle'; + +import { + COST_INSIGHT_ROLLUP_VERSION, + getCanonicalOwnerSpendTotals, + parseSafeDatabaseInteger, + requireUtcHour, + requireUtcTimestamp, + type CostInsightQueryExecutor, + type CostInsightSpendCategory, + type CostInsightSpendSource, +} from './canonical-sources'; + +const HOUR_MS = 60 * 60 * 1_000; +export const COST_INSIGHT_MAX_HOURLY_BUCKETS = 2_160; +export const COST_INSIGHT_MAX_TOP_DRIVERS = 5; + +export type OwnerHourlySpend = { + hourStart: string; + variableMicrodollars: number | null; + scheduledMicrodollars: number | null; + totalMicrodollars: number | null; + variableRecordCount: number | null; + scheduledRecordCount: number | null; + isCovered: boolean; +}; + +export type OwnerTopSpendDriver = { + category: CostInsightSpendCategory; + source: CostInsightSpendSource; + productKey: string; + featureKey: string; + modelOrPlanKey: string; + providerKey: string; + actorUserId: string; + totalMicrodollars: number; + spendRecordCount: number; +}; + +export type CostInsightDegradedInterval = { + id: string; + startHour: string; + endHourExclusive: string; + source: CostInsightSpendSource | null; + reason: string; + detectedAt: string; +}; + +export type CostInsightRollupCoverage = { + rollupVersion: number; + liveCaptureStartHour: string | null; + coverageStartHour: string | null; + lastReconciledAt: string | null; + degradedIntervals: CostInsightDegradedInterval[]; + isFullyCovered: boolean; +}; + +export type OwnerRolling24HourSpendExact = { + asOf: string; + windowStart: string; + variableMicrodollars: number | null; + scheduledMicrodollars: number | null; + totalMicrodollars: number | null; + isComplete: boolean; +}; + +export type Rolling24HourFragments = { + asOf: string; + windowStart: string; + oldestBoundaryEnd: string; + interiorStart: string; + interiorEnd: string; + currentBoundaryStart: string; +}; + +type DenseHourlySpendRow = { + hour_start: string | Date; + variable_microdollars: string | number | bigint | null; + scheduled_microdollars: string | number | bigint | null; + variable_record_count: string | number | bigint | null; + scheduled_record_count: string | number | bigint | null; + is_covered: boolean; +}; + +type TopDriverRow = { + spend_category: CostInsightSpendCategory; + source: CostInsightSpendSource; + product_key: string; + feature_key: string; + model_or_plan_key: string; + provider_key: string; + actor_user_id: string; + total_microdollars: string | number | bigint; + spend_record_count: string | number | bigint; +}; + +type CurrentHourRow = { + variable_microdollars: string | number | bigint; + scheduled_microdollars: string | number | bigint; + variable_record_count: string | number | bigint; + scheduled_record_count: string | number | bigint; +}; + +type CoverageRow = { + rollup_version: string | number | bigint; + live_capture_start_hour: string | Date | null; + coverage_start_hour: string | Date | null; + last_reconciled_at: string | Date | null; + database_now: string | Date; +}; + +type DegradedIntervalRow = { + id: string; + start_hour: string | Date; + end_hour_exclusive: string | Date; + source: CostInsightSpendSource | null; + reason: string; + detected_at: string | Date; +}; + +type InteriorTotalRow = { + spend_category: CostInsightSpendCategory; + total_microdollars: string | number | bigint; +}; + +type DatabaseTimestampRow = { + value: string | Date; +}; + +type ExactRollingDatabase = Pick; + +function ownerPredicate( + owner: CostInsightSpendOwner, + ownedByUserId: SQL, + ownedByOrganizationId: SQL +): SQL { + return owner.type === 'organization' + ? sql`${ownedByUserId} IS NULL AND ${ownedByOrganizationId} = ${owner.id}` + : sql`${ownedByOrganizationId} IS NULL AND ${ownedByUserId} = ${owner.id}`; +} + +function normalizeDatabaseTimestamp(value: string | Date, fieldName: string): string { + const timestamp = value instanceof Date ? value.getTime() : Date.parse(value); + if (!Number.isFinite(timestamp)) { + throw new Error(`${fieldName} is not a valid timestamp.`); + } + return new Date(timestamp).toISOString(); +} + +function normalizeNullableDatabaseTimestamp( + value: string | Date | null, + fieldName: string +): string | null { + return value === null ? null : normalizeDatabaseTimestamp(value, fieldName); +} + +function requireHourlyRange(params: { + startHour: string; + endHourExclusive: string; + maxBuckets?: number; +}): { startHour: string; endHourExclusive: string; bucketCount: number } { + const startHour = requireUtcHour(params.startHour, 'startHour'); + const endHourExclusive = requireUtcHour(params.endHourExclusive, 'endHourExclusive'); + const bucketCount = (Date.parse(endHourExclusive) - Date.parse(startHour)) / HOUR_MS; + const maxBuckets = params.maxBuckets ?? COST_INSIGHT_MAX_HOURLY_BUCKETS; + if (!Number.isInteger(bucketCount) || bucketCount <= 0 || bucketCount > maxBuckets) { + throw new Error(`Cost Insights range must contain between 1 and ${maxBuckets} UTC hours.`); + } + return { startHour, endHourExclusive, bucketCount }; +} + +function sumSafe(left: number, right: number, fieldName: string): number { + const total = left + right; + if (!Number.isSafeInteger(total)) { + throw new Error(`${fieldName} is outside the JavaScript safe-integer range.`); + } + return total; +} + +function floorUtcHour(timestamp: number): number { + return Math.floor(timestamp / HOUR_MS) * HOUR_MS; +} + +function ceilUtcHour(timestamp: number): number { + return Math.ceil(timestamp / HOUR_MS) * HOUR_MS; +} + +export function getRolling24HourFragments(asOfInput: string): Rolling24HourFragments { + const asOf = requireUtcTimestamp(asOfInput, 'asOf'); + const asOfTimestamp = Date.parse(asOf); + const windowStartTimestamp = asOfTimestamp - 24 * HOUR_MS; + const oldestBoundaryEndTimestamp = ceilUtcHour(windowStartTimestamp); + const currentBoundaryStartTimestamp = floorUtcHour(asOfTimestamp); + return { + asOf, + windowStart: new Date(windowStartTimestamp).toISOString(), + oldestBoundaryEnd: new Date(oldestBoundaryEndTimestamp).toISOString(), + interiorStart: new Date(oldestBoundaryEndTimestamp).toISOString(), + interiorEnd: new Date(currentBoundaryStartTimestamp).toISOString(), + currentBoundaryStart: new Date(currentBoundaryStartTimestamp).toISOString(), + }; +} + +export async function getOwnerHourlySpend( + executor: CostInsightQueryExecutor, + params: { + owner: CostInsightSpendOwner; + startHour: string; + endHourExclusive: string; + } +): Promise { + const range = requireHourlyRange(params); + const owner = params.owner; + const result = await executor.execute(sql` + WITH hours AS ( + SELECT generate_series( + ${range.startHour}::timestamptz, + ${range.endHourExclusive}::timestamptz - INTERVAL '1 hour', + INTERVAL '1 hour' + ) AS hour_start + ), coverage_status AS ( + SELECT + hours.hour_start, + COALESCE( + hours.hour_start >= COALESCE( + ${cost_insight_rollup_coverage.coverage_start_hour}, + ${cost_insight_rollup_coverage.live_capture_start_hour} + ) + AND hours.hour_start <= date_trunc('hour', CURRENT_TIMESTAMP, 'UTC') + AND NOT EXISTS ( + SELECT 1 + FROM ${cost_insight_rollup_degraded_intervals} degraded + WHERE degraded.resolved_at IS NULL + AND degraded.start_hour < hours.hour_start + INTERVAL '1 hour' + AND degraded.end_hour_exclusive > hours.hour_start + ), + FALSE + ) AS is_covered + FROM hours + LEFT JOIN ${cost_insight_rollup_coverage} + ON ${cost_insight_rollup_coverage.rollup_version} = ${COST_INSIGHT_ROLLUP_VERSION} + ) + SELECT + coverage_status.hour_start, + CASE WHEN coverage_status.is_covered + THEN COALESCE(variable_total.total_microdollars, 0)::text ELSE NULL END + AS variable_microdollars, + CASE WHEN coverage_status.is_covered + THEN COALESCE(scheduled_total.total_microdollars, 0)::text ELSE NULL END + AS scheduled_microdollars, + CASE WHEN coverage_status.is_covered + THEN COALESCE(variable_total.spend_record_count, 0)::text ELSE NULL END + AS variable_record_count, + CASE WHEN coverage_status.is_covered + THEN COALESCE(scheduled_total.spend_record_count, 0)::text ELSE NULL END + AS scheduled_record_count, + coverage_status.is_covered + FROM coverage_status + LEFT JOIN ${cost_insight_owner_hour_totals} variable_total + ON variable_total.hour_start = coverage_status.hour_start + AND variable_total.spend_category = 'variable' + AND ${ownerPredicate( + owner, + sql`variable_total.owned_by_user_id`, + sql`variable_total.owned_by_organization_id` + )} + LEFT JOIN ${cost_insight_owner_hour_totals} scheduled_total + ON scheduled_total.hour_start = coverage_status.hour_start + AND scheduled_total.spend_category = 'scheduled' + AND ${ownerPredicate( + owner, + sql`scheduled_total.owned_by_user_id`, + sql`scheduled_total.owned_by_organization_id` + )} + ORDER BY coverage_status.hour_start ASC + `); + + return result.rows.map(row => { + const hourStart = normalizeDatabaseTimestamp(row.hour_start, 'hour_start'); + if (!row.is_covered) { + return { + hourStart, + variableMicrodollars: null, + scheduledMicrodollars: null, + totalMicrodollars: null, + variableRecordCount: null, + scheduledRecordCount: null, + isCovered: false, + }; + } + if ( + row.variable_microdollars === null || + row.scheduled_microdollars === null || + row.variable_record_count === null || + row.scheduled_record_count === null + ) { + throw new Error('Covered Cost Insights hour returned incomplete aggregate values.'); + } + const variableMicrodollars = parseSafeDatabaseInteger( + row.variable_microdollars, + 'variable_microdollars' + ); + const scheduledMicrodollars = parseSafeDatabaseInteger( + row.scheduled_microdollars, + 'scheduled_microdollars' + ); + return { + hourStart, + variableMicrodollars, + scheduledMicrodollars, + totalMicrodollars: sumSafe( + variableMicrodollars, + scheduledMicrodollars, + 'hourly total microdollars' + ), + variableRecordCount: parseSafeDatabaseInteger( + row.variable_record_count, + 'variable_record_count' + ), + scheduledRecordCount: parseSafeDatabaseInteger( + row.scheduled_record_count, + 'scheduled_record_count' + ), + isCovered: true, + }; + }); +} + +export async function getOwnerTopSpendDrivers( + executor: CostInsightQueryExecutor, + params: { + owner: CostInsightSpendOwner; + startHour: string; + endHourExclusive: string; + category?: CostInsightSpendCategory; + limit?: number; + } +): Promise { + const range = requireHourlyRange(params); + const requestedLimit = params.limit ?? COST_INSIGHT_MAX_TOP_DRIVERS; + if (!Number.isSafeInteger(requestedLimit) || requestedLimit <= 0) { + throw new Error('Cost Insights top-driver limit must be a positive safe integer.'); + } + const limit = Math.min(requestedLimit, COST_INSIGHT_MAX_TOP_DRIVERS); + const categoryPredicate = params.category + ? sql`${cost_insight_owner_hour_driver_buckets.spend_category} = ${params.category}` + : sql`TRUE`; + const owner = params.owner; + const result = await executor.execute(sql` + SELECT + ${cost_insight_owner_hour_driver_buckets.spend_category} AS spend_category, + ${cost_insight_owner_hour_driver_buckets.source} AS source, + ${cost_insight_owner_hour_driver_buckets.product_key} AS product_key, + ${cost_insight_owner_hour_driver_buckets.feature_key} AS feature_key, + ${cost_insight_owner_hour_driver_buckets.model_or_plan_key} AS model_or_plan_key, + ${cost_insight_owner_hour_driver_buckets.provider_key} AS provider_key, + ${cost_insight_owner_hour_driver_buckets.actor_user_id} AS actor_user_id, + SUM(${cost_insight_owner_hour_driver_buckets.total_microdollars})::text + AS total_microdollars, + SUM(${cost_insight_owner_hour_driver_buckets.spend_record_count})::text + AS spend_record_count + FROM ${cost_insight_owner_hour_driver_buckets} + WHERE ${cost_insight_owner_hour_driver_buckets.hour_start} >= ${range.startHour} + AND ${cost_insight_owner_hour_driver_buckets.hour_start} < ${range.endHourExclusive} + AND ${ownerPredicate( + owner, + sql`${cost_insight_owner_hour_driver_buckets.owned_by_user_id}`, + sql`${cost_insight_owner_hour_driver_buckets.owned_by_organization_id}` + )} + AND ${categoryPredicate} + GROUP BY 1, 2, 3, 4, 5, 6, 7 + ORDER BY + SUM(${cost_insight_owner_hour_driver_buckets.total_microdollars}) DESC, + ${cost_insight_owner_hour_driver_buckets.spend_category} ASC, + ${cost_insight_owner_hour_driver_buckets.source} ASC, + ${cost_insight_owner_hour_driver_buckets.product_key} ASC, + ${cost_insight_owner_hour_driver_buckets.feature_key} ASC, + ${cost_insight_owner_hour_driver_buckets.model_or_plan_key} ASC, + ${cost_insight_owner_hour_driver_buckets.provider_key} ASC, + ${cost_insight_owner_hour_driver_buckets.actor_user_id} ASC + LIMIT ${limit} + `); + return result.rows.map(row => ({ + category: row.spend_category, + source: row.source, + productKey: row.product_key, + featureKey: row.feature_key, + modelOrPlanKey: row.model_or_plan_key, + providerKey: row.provider_key, + actorUserId: row.actor_user_id, + totalMicrodollars: parseSafeDatabaseInteger( + row.total_microdollars, + 'top-driver total_microdollars' + ), + spendRecordCount: parseSafeDatabaseInteger( + row.spend_record_count, + 'top-driver spend_record_count' + ), + })); +} + +export async function getOwnerCurrentHourSpend( + primaryExecutor: CostInsightQueryExecutor, + owner: CostInsightSpendOwner +): Promise<{ + variableMicrodollars: number; + scheduledMicrodollars: number; + totalMicrodollars: number; + variableRecordCount: number; + scheduledRecordCount: number; +}> { + const result = await primaryExecutor.execute(sql` + SELECT + COALESCE(SUM(total_microdollars) FILTER (WHERE spend_category = 'variable'), 0)::text + AS variable_microdollars, + COALESCE(SUM(total_microdollars) FILTER (WHERE spend_category = 'scheduled'), 0)::text + AS scheduled_microdollars, + COALESCE(SUM(spend_record_count) FILTER (WHERE spend_category = 'variable'), 0)::text + AS variable_record_count, + COALESCE(SUM(spend_record_count) FILTER (WHERE spend_category = 'scheduled'), 0)::text + AS scheduled_record_count + FROM ${cost_insight_owner_hour_totals} + WHERE ${cost_insight_owner_hour_totals.hour_start} = date_trunc( + 'hour', CURRENT_TIMESTAMP, 'UTC' + ) + AND ${ownerPredicate( + owner, + sql`${cost_insight_owner_hour_totals.owned_by_user_id}`, + sql`${cost_insight_owner_hour_totals.owned_by_organization_id}` + )} + `); + const row = result.rows[0]; + if (!row) { + throw new Error('Cost Insights current-hour query returned no aggregate row.'); + } + const variableMicrodollars = parseSafeDatabaseInteger( + row.variable_microdollars, + 'current-hour variable_microdollars' + ); + const scheduledMicrodollars = parseSafeDatabaseInteger( + row.scheduled_microdollars, + 'current-hour scheduled_microdollars' + ); + return { + variableMicrodollars, + scheduledMicrodollars, + totalMicrodollars: sumSafe( + variableMicrodollars, + scheduledMicrodollars, + 'current-hour total microdollars' + ), + variableRecordCount: parseSafeDatabaseInteger( + row.variable_record_count, + 'current-hour variable_record_count' + ), + scheduledRecordCount: parseSafeDatabaseInteger( + row.scheduled_record_count, + 'current-hour scheduled_record_count' + ), + }; +} + +export async function getCostInsightRollupCoverage( + executor: CostInsightQueryExecutor, + params: { startHour: string; endHourExclusive: string } +): Promise { + const range = requireHourlyRange(params); + const coverageResult = await executor.execute(sql` + SELECT + ${cost_insight_rollup_coverage.rollup_version} AS rollup_version, + ${cost_insight_rollup_coverage.live_capture_start_hour} AS live_capture_start_hour, + ${cost_insight_rollup_coverage.coverage_start_hour} AS coverage_start_hour, + ${cost_insight_rollup_coverage.last_reconciled_at} AS last_reconciled_at, + CURRENT_TIMESTAMP AS database_now + FROM ${cost_insight_rollup_coverage} + WHERE ${cost_insight_rollup_coverage.rollup_version} = ${COST_INSIGHT_ROLLUP_VERSION} + LIMIT 1 + `); + const degradedResult = await executor.execute(sql` + SELECT + ${cost_insight_rollup_degraded_intervals.id} AS id, + ${cost_insight_rollup_degraded_intervals.start_hour} AS start_hour, + ${cost_insight_rollup_degraded_intervals.end_hour_exclusive} AS end_hour_exclusive, + ${cost_insight_rollup_degraded_intervals.source} AS source, + ${cost_insight_rollup_degraded_intervals.reason} AS reason, + ${cost_insight_rollup_degraded_intervals.detected_at} AS detected_at + FROM ${cost_insight_rollup_degraded_intervals} + WHERE ${cost_insight_rollup_degraded_intervals.resolved_at} IS NULL + AND ${cost_insight_rollup_degraded_intervals.start_hour} < ${range.endHourExclusive} + AND ${cost_insight_rollup_degraded_intervals.end_hour_exclusive} > ${range.startHour} + ORDER BY + ${cost_insight_rollup_degraded_intervals.start_hour} ASC, + ${cost_insight_rollup_degraded_intervals.id} ASC + `); + + const coverageRow = coverageResult.rows[0]; + const liveCaptureStartHour = coverageRow + ? normalizeNullableDatabaseTimestamp( + coverageRow.live_capture_start_hour, + 'live_capture_start_hour' + ) + : null; + const coverageStartHour = coverageRow + ? normalizeNullableDatabaseTimestamp(coverageRow.coverage_start_hour, 'coverage_start_hour') + : null; + const effectiveCoverageStart = coverageStartHour ?? liveCaptureStartHour; + const databaseNow = coverageRow + ? Date.parse(normalizeDatabaseTimestamp(coverageRow.database_now, 'database_now')) + : Number.NEGATIVE_INFINITY; + const latestCoveredEnd = ceilUtcHour(databaseNow); + const degradedIntervals = degradedResult.rows.map(row => ({ + id: row.id, + startHour: normalizeDatabaseTimestamp(row.start_hour, 'degraded start_hour'), + endHourExclusive: normalizeDatabaseTimestamp( + row.end_hour_exclusive, + 'degraded end_hour_exclusive' + ), + source: row.source, + reason: row.reason, + detectedAt: normalizeDatabaseTimestamp(row.detected_at, 'degraded detected_at'), + })); + + return { + rollupVersion: coverageRow + ? parseSafeDatabaseInteger(coverageRow.rollup_version, 'rollup_version') + : COST_INSIGHT_ROLLUP_VERSION, + liveCaptureStartHour, + coverageStartHour, + lastReconciledAt: coverageRow + ? normalizeNullableDatabaseTimestamp(coverageRow.last_reconciled_at, 'last_reconciled_at') + : null, + degradedIntervals, + isFullyCovered: + effectiveCoverageStart !== null && + Date.parse(range.startHour) >= Date.parse(effectiveCoverageStart) && + Date.parse(range.endHourExclusive) <= latestCoveredEnd && + degradedIntervals.length === 0, + }; +} + +async function getInteriorRollupTotals( + executor: CostInsightQueryExecutor, + owner: CostInsightSpendOwner, + startInclusive: string, + endExclusive: string +): Promise<{ variableMicrodollars: number; scheduledMicrodollars: number }> { + if (startInclusive === endExclusive) { + return { variableMicrodollars: 0, scheduledMicrodollars: 0 }; + } + const result = await executor.execute(sql` + SELECT + ${cost_insight_owner_hour_totals.spend_category} AS spend_category, + SUM(${cost_insight_owner_hour_totals.total_microdollars})::text AS total_microdollars + FROM ${cost_insight_owner_hour_totals} + WHERE ${cost_insight_owner_hour_totals.hour_start} >= ${startInclusive} + AND ${cost_insight_owner_hour_totals.hour_start} < ${endExclusive} + AND ${ownerPredicate( + owner, + sql`${cost_insight_owner_hour_totals.owned_by_user_id}`, + sql`${cost_insight_owner_hour_totals.owned_by_organization_id}` + )} + GROUP BY ${cost_insight_owner_hour_totals.spend_category} + `); + let variableMicrodollars = 0; + let scheduledMicrodollars = 0; + for (const row of result.rows) { + const amount = parseSafeDatabaseInteger( + row.total_microdollars, + 'rolling interior total_microdollars' + ); + if (row.spend_category === 'variable') { + variableMicrodollars = amount; + } else if (row.spend_category === 'scheduled') { + scheduledMicrodollars = amount; + } + } + return { variableMicrodollars, scheduledMicrodollars }; +} + +export async function getOwnerRolling24HourSpendExact( + primaryDatabase: ExactRollingDatabase, + params: { owner: CostInsightSpendOwner; asOf?: string } +): Promise { + const requestedAsOf = + params.asOf === undefined ? undefined : requireUtcTimestamp(params.asOf, 'asOf'); + + return primaryDatabase.transaction( + async transaction => { + const asOfResult = await transaction.execute(sql` + SELECT COALESCE(${requestedAsOf ?? null}::timestamptz, CURRENT_TIMESTAMP) AS value + `); + const asOfRow = asOfResult.rows[0]; + if (!asOfRow) { + throw new Error('Cost Insights exact rolling query could not establish an as-of value.'); + } + const fragments = getRolling24HourFragments( + normalizeDatabaseTimestamp(asOfRow.value, 'as_of') + ); + const { + asOf, + windowStart, + oldestBoundaryEnd, + interiorStart, + interiorEnd, + currentBoundaryStart, + } = fragments; + + const coverage = + interiorStart === interiorEnd + ? null + : await getCostInsightRollupCoverage(transaction, { + startHour: interiorStart, + endHourExclusive: interiorEnd, + }); + if (coverage && !coverage.isFullyCovered) { + return { + asOf, + windowStart, + variableMicrodollars: null, + scheduledMicrodollars: null, + totalMicrodollars: null, + isComplete: false, + }; + } + + const interior = await getInteriorRollupTotals( + transaction, + params.owner, + interiorStart, + interiorEnd + ); + const oldestBoundary = + windowStart === oldestBoundaryEnd + ? { + variableMicrodollars: 0, + scheduledMicrodollars: 0, + } + : await getCanonicalOwnerSpendTotals(transaction, { + owner: params.owner, + startInclusive: windowStart, + endExclusive: interiorStart, + }); + const currentBoundary = + currentBoundaryStart === asOf + ? { + variableMicrodollars: 0, + scheduledMicrodollars: 0, + } + : await getCanonicalOwnerSpendTotals(transaction, { + owner: params.owner, + startInclusive: interiorEnd, + endExclusive: asOf, + }); + const variableMicrodollars = sumSafe( + sumSafe( + interior.variableMicrodollars, + oldestBoundary.variableMicrodollars, + 'rolling variable microdollars' + ), + currentBoundary.variableMicrodollars, + 'rolling variable microdollars' + ); + const scheduledMicrodollars = sumSafe( + sumSafe( + interior.scheduledMicrodollars, + oldestBoundary.scheduledMicrodollars, + 'rolling scheduled microdollars' + ), + currentBoundary.scheduledMicrodollars, + 'rolling scheduled microdollars' + ); + return { + asOf, + windowStart, + variableMicrodollars, + scheduledMicrodollars, + totalMicrodollars: sumSafe( + variableMicrodollars, + scheduledMicrodollars, + 'rolling total microdollars' + ), + isComplete: true, + }; + }, + { isolationLevel: 'repeatable read', accessMode: 'read only' } + ); +} diff --git a/apps/web/src/lib/cost-insights/spend-writer-audit.test.ts b/apps/web/src/lib/cost-insights/spend-writer-audit.test.ts new file mode 100644 index 0000000000..06319ee0aa --- /dev/null +++ b/apps/web/src/lib/cost-insights/spend-writer-audit.test.ts @@ -0,0 +1,57 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { join, relative, resolve } from 'node:path'; + +const repositoryRoot = resolve(process.cwd(), '../..'); +const sourceRoots = ['apps', 'dev', 'packages', 'services']; + +const classifiedIncrementWriters = { + 'apps/web/src/lib/ai-gateway/processUsage.ts': 'included_ai_gateway_personal', + 'apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts': 'included_coding_plan_renewal', + 'apps/web/src/lib/coding-plans/index.ts': 'included_coding_plan_activation', + 'apps/web/src/lib/exa-usage.ts': 'included_exa_personal', + 'apps/web/src/lib/kiloclaw/credit-billing.ts': 'included_kiloclaw_enrollment', + 'apps/web/src/lib/organizations/organization-usage.ts': + 'included_ai_gateway_and_exa_organization', + 'services/kiloclaw-billing/src/lifecycle.ts': 'included_kiloclaw_renewal', + 'apps/web/src/app/admin/api/organizations/[id]/consume-credits/route.ts': + 'excluded_development_consume_route', + 'apps/web/src/routers/admin-router.ts': 'excluded_development_balance_jitter', + 'dev/seed/kiloclaw/fake-instance.ts': 'excluded_development_seed', +} as const; + +const rawSqlIncrement = /\bSET\s+microdollars_used\s*=\s*microdollars_used\s*\+/; +const drizzleIncrement = /\bmicrodollars_used\s*:\s*sql`[^`]*\bmicrodollars_used\b[^`]*\+/; + +function listTypeScriptFiles(directory: string): string[] { + const files: string[] = []; + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === '.next' || entry.name === 'dist') continue; + const path = join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...listTypeScriptFiles(path)); + } else if ( + entry.isFile() && + path.endsWith('.ts') && + !path.endsWith('.test.ts') && + !path.endsWith('.spec.ts') + ) { + files.push(path); + } + } + return files; +} + +describe('Cost Insights Credit-spend writer audit', () => { + test('requires every direct microdollars_used increment to have an explicit classification', () => { + const detectedWriters = sourceRoots + .flatMap(sourceRoot => listTypeScriptFiles(join(repositoryRoot, sourceRoot))) + .filter(path => { + const source = readFileSync(path, 'utf8'); + return rawSqlIncrement.test(source) || drizzleIncrement.test(source); + }) + .map(path => relative(repositoryRoot, path)) + .sort(); + + expect(detectedWriters).toEqual(Object.keys(classifiedIncrementWriters).sort()); + }); +}); diff --git a/apps/web/src/lib/exa-paths.ts b/apps/web/src/lib/exa-paths.ts new file mode 100644 index 0000000000..5b2347e57d --- /dev/null +++ b/apps/web/src/lib/exa-paths.ts @@ -0,0 +1,27 @@ +export const EXA_ALLOWED_PATHS = [ + '/search', + '/contents', + '/findSimilar', + '/answer', + '/context', +] as const; + +export type ExaAllowedPath = (typeof EXA_ALLOWED_PATHS)[number]; + +const exaAllowedPathSet: ReadonlySet = new Set(EXA_ALLOWED_PATHS); + +export function isExaAllowedPath(path: string): path is ExaAllowedPath { + return exaAllowedPathSet.has(path); +} + +const exaCostInsightFeatureKeyByPath: Record = { + '/search': 'search', + '/contents': 'contents', + '/findSimilar': 'findSimilar', + '/answer': 'answer', + '/context': 'context', +}; + +export function getExaCostInsightFeatureKey(path: string): string { + return isExaAllowedPath(path) ? exaCostInsightFeatureKeyByPath[path] : 'other'; +} diff --git a/apps/web/src/lib/exa-usage-log-indexes-script.test.ts b/apps/web/src/lib/exa-usage-log-indexes-script.test.ts new file mode 100644 index 0000000000..00f57f40fa --- /dev/null +++ b/apps/web/src/lib/exa-usage-log-indexes-script.test.ts @@ -0,0 +1,109 @@ +import { + parseExaUsageLogIndexScriptArgs, + provisionHistoricalExaUsageLogIndexes, +} from '@/scripts/db/exa-usage-log-indexes'; +import type { SQL } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; + +describe('Exa usage-log index operator', () => { + test('defaults to dry-run without a partition limit or pacing', () => { + expect(parseExaUsageLogIndexScriptArgs([])).toEqual({ + execute: false, + sleepMs: 0, + }); + }); + + test('parses bounded execution and pacing options', () => { + expect( + parseExaUsageLogIndexScriptArgs(['--execute', '--max-partitions', '3', '--sleep-ms', '250']) + ).toEqual({ + execute: true, + maxPartitions: 3, + sleepMs: 250, + }); + }); + + test('rejects unsafe or ambiguous arguments', () => { + expect(() => parseExaUsageLogIndexScriptArgs(['--max-partitions', '0'])).toThrow( + '--max-partitions must be a positive safe integer' + ); + expect(() => parseExaUsageLogIndexScriptArgs(['--sleep-ms', '-1'])).toThrow( + '--sleep-ms must be a non-negative integer' + ); + expect(() => parseExaUsageLogIndexScriptArgs(['--execute', '--execute'])).toThrow( + 'Duplicate flag: --execute' + ); + expect(() => parseExaUsageLogIndexScriptArgs(['--all'])).toThrow('Unknown flag: --all'); + }); + + test('dry-run reads catalog partitions without executing index DDL', async () => { + const statements: string[] = []; + const dialect = new PgDialect(); + const fakeDb = { + execute: async (query: SQL) => { + statements.push(dialect.sqlToQuery(query).sql); + return { + rows: [{ schema_name: 'public', partition_name: 'exa_usage_log_2026_06' }], + }; + }, + }; + const log = jest.spyOn(console, 'log').mockImplementation(() => undefined); + + try { + await provisionHistoricalExaUsageLogIndexes(fakeDb as never, { + execute: false, + sleepMs: 0, + }); + } finally { + log.mockRestore(); + } + + expect(statements).toHaveLength(1); + expect(statements[0]).toContain('pg_catalog.pg_partition_tree'); + expect(statements[0]).toContain('partition_tree.isleaf'); + expect(statements[0]).toContain('partition_class.relispartition'); + }); + + test('executes both concurrent indexes sequentially for selected catalog partitions', async () => { + const statements: string[] = []; + const dialect = new PgDialect(); + let activeExecutions = 0; + let maximumActiveExecutions = 0; + const fakeDb = { + execute: async (query: SQL) => { + const statement = dialect.sqlToQuery(query).sql; + statements.push(statement); + if (statements.length === 1) { + return { + rows: [ + { schema_name: 'public', partition_name: 'exa_usage_log_2026_06' }, + { schema_name: 'public', partition_name: 'exa_usage_log_2026_05' }, + ], + }; + } + activeExecutions++; + maximumActiveExecutions = Math.max(maximumActiveExecutions, activeExecutions); + await Promise.resolve(); + activeExecutions--; + return { rows: [] }; + }, + }; + const log = jest.spyOn(console, 'log').mockImplementation(() => undefined); + + try { + await provisionHistoricalExaUsageLogIndexes(fakeDb as never, { + execute: true, + maxPartitions: 1, + sleepMs: 0, + }); + } finally { + log.mockRestore(); + } + + expect(maximumActiveExecutions).toBe(1); + expect(statements.slice(1)).toEqual([ + 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "exa_usage_log_2026_06_charged_created_at_idx" ON "public"."exa_usage_log_2026_06" ("created_at") WHERE "charged_to_balance" = true AND "cost_microdollars" > 0', + 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "exa_usage_log_2026_06_charged_org_created_at_idx" ON "public"."exa_usage_log_2026_06" ("organization_id", "created_at") WHERE "organization_id" IS NOT NULL AND "charged_to_balance" = true AND "cost_microdollars" > 0', + ]); + }); +}); diff --git a/apps/web/src/lib/exa-usage-partitions.test.ts b/apps/web/src/lib/exa-usage-partitions.test.ts new file mode 100644 index 0000000000..17bdc25f56 --- /dev/null +++ b/apps/web/src/lib/exa-usage-partitions.test.ts @@ -0,0 +1,84 @@ +import { + buildExaUsageLogPartitionIndexDefinitions, + provisionExaUsageLogPartitions, +} from '@/lib/exa-usage-partitions'; +import type { SQL } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; + +describe('Exa usage-log partition indexes', () => { + test('builds regular local partial indexes for future partitions', () => { + expect( + buildExaUsageLogPartitionIndexDefinitions('public', 'exa_usage_log_2026_07', false) + ).toEqual([ + { + name: 'exa_usage_log_2026_07_charged_created_at_idx', + statement: + 'CREATE INDEX IF NOT EXISTS "exa_usage_log_2026_07_charged_created_at_idx" ON "public"."exa_usage_log_2026_07" ("created_at") WHERE "charged_to_balance" = true AND "cost_microdollars" > 0', + }, + { + name: 'exa_usage_log_2026_07_charged_org_created_at_idx', + statement: + 'CREATE INDEX IF NOT EXISTS "exa_usage_log_2026_07_charged_org_created_at_idx" ON "public"."exa_usage_log_2026_07" ("organization_id", "created_at") WHERE "organization_id" IS NOT NULL AND "charged_to_balance" = true AND "cost_microdollars" > 0', + }, + ]); + }); + + test('builds concurrent statements for the historical operator path', () => { + const definitions = buildExaUsageLogPartitionIndexDefinitions( + 'public', + 'exa_usage_log_2026_06', + true + ); + + expect(definitions).toHaveLength(2); + expect( + definitions.every(({ statement }) => + statement.startsWith('CREATE INDEX CONCURRENTLY IF NOT EXISTS') + ) + ).toBe(true); + }); + + test('rejects identifiers outside the expected catalog naming contract', () => { + expect(() => + buildExaUsageLogPartitionIndexDefinitions( + 'public', + 'exa_usage_log_2026_07"; DROP TABLE exa_usage_log; --', + true + ) + ).toThrow('Invalid Exa usage-log partition name'); + expect(() => + buildExaUsageLogPartitionIndexDefinitions( + 'public"; DROP SCHEMA public; --', + 'exa_usage_log_2026_07', + true + ) + ).toThrow('Unsafe PostgreSQL identifier'); + }); + + test('provisions indexes only on write-free future partitions', async () => { + const statements: string[] = []; + const dialect = new PgDialect(); + const fakeDb = { + execute: async (query: SQL) => { + statements.push(dialect.sqlToQuery(query).sql); + return { rows: [] }; + }, + }; + + const result = await provisionExaUsageLogPartitions(fakeDb as never, new Date(2026, 5, 15, 12)); + + expect(result).toEqual({ + created: ['exa_usage_log_2026_06', 'exa_usage_log_2026_07', 'exa_usage_log_2026_08'], + errors: [], + }); + expect(statements).toEqual([ + 'CREATE TABLE IF NOT EXISTS "public"."exa_usage_log_2026_06" PARTITION OF "public"."exa_usage_log" FOR VALUES FROM (\'2026-06-01\') TO (\'2026-07-01\')', + 'CREATE TABLE IF NOT EXISTS "public"."exa_usage_log_2026_07" PARTITION OF "public"."exa_usage_log" FOR VALUES FROM (\'2026-07-01\') TO (\'2026-08-01\')', + 'CREATE INDEX IF NOT EXISTS "exa_usage_log_2026_07_charged_created_at_idx" ON "public"."exa_usage_log_2026_07" ("created_at") WHERE "charged_to_balance" = true AND "cost_microdollars" > 0', + 'CREATE INDEX IF NOT EXISTS "exa_usage_log_2026_07_charged_org_created_at_idx" ON "public"."exa_usage_log_2026_07" ("organization_id", "created_at") WHERE "organization_id" IS NOT NULL AND "charged_to_balance" = true AND "cost_microdollars" > 0', + 'CREATE TABLE IF NOT EXISTS "public"."exa_usage_log_2026_08" PARTITION OF "public"."exa_usage_log" FOR VALUES FROM (\'2026-08-01\') TO (\'2026-09-01\')', + 'CREATE INDEX IF NOT EXISTS "exa_usage_log_2026_08_charged_created_at_idx" ON "public"."exa_usage_log_2026_08" ("created_at") WHERE "charged_to_balance" = true AND "cost_microdollars" > 0', + 'CREATE INDEX IF NOT EXISTS "exa_usage_log_2026_08_charged_org_created_at_idx" ON "public"."exa_usage_log_2026_08" ("organization_id", "created_at") WHERE "organization_id" IS NOT NULL AND "charged_to_balance" = true AND "cost_microdollars" > 0', + ]); + }); +}); diff --git a/apps/web/src/lib/exa-usage-partitions.ts b/apps/web/src/lib/exa-usage-partitions.ts index 05dd5afa41..97d895c9f4 100644 --- a/apps/web/src/lib/exa-usage-partitions.ts +++ b/apps/web/src/lib/exa-usage-partitions.ts @@ -9,6 +9,56 @@ export type ExaUsageLogPartitionProvisioningResult = { errors: Array<{ name: string; error: unknown }>; }; +export type ExaUsageLogPartitionIndexDefinition = { + name: string; + statement: string; +}; + +const POSTGRES_IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_]*$/; +const EXA_USAGE_LOG_PARTITION_PATTERN = /^exa_usage_log_\d{4}_(?:0[1-9]|1[0-2])$/; +const POSTGRES_IDENTIFIER_MAX_LENGTH = 63; + +function quoteIdentifier(identifier: string): string { + if ( + identifier.length > POSTGRES_IDENTIFIER_MAX_LENGTH || + !POSTGRES_IDENTIFIER_PATTERN.test(identifier) + ) { + throw new Error(`Unsafe PostgreSQL identifier: ${identifier}`); + } + return `"${identifier}"`; +} + +export function buildExaUsageLogPartitionIndexDefinitions( + schemaName: string, + partitionName: string, + concurrently: boolean +): ExaUsageLogPartitionIndexDefinition[] { + if (!EXA_USAGE_LOG_PARTITION_PATTERN.test(partitionName)) { + throw new Error(`Invalid Exa usage-log partition name: ${partitionName}`); + } + + const tableName = `${quoteIdentifier(schemaName)}.${quoteIdentifier(partitionName)}`; + const concurrentlyClause = concurrently ? ' CONCURRENTLY' : ''; + const definitions = [ + { + name: `${partitionName}_charged_created_at_idx`, + columns: '"created_at"', + predicate: '"charged_to_balance" = true AND "cost_microdollars" > 0', + }, + { + name: `${partitionName}_charged_org_created_at_idx`, + columns: '"organization_id", "created_at"', + predicate: + '"organization_id" IS NOT NULL AND "charged_to_balance" = true AND "cost_microdollars" > 0', + }, + ]; + + return definitions.map(definition => ({ + name: definition.name, + statement: `CREATE INDEX${concurrentlyClause} IF NOT EXISTS ${quoteIdentifier(definition.name)} ON ${tableName} (${definition.columns}) WHERE ${definition.predicate}`, + })); +} + /** * Creates the current month and next two monthly audit-log partitions. * @@ -30,9 +80,19 @@ export async function provisionExaUsageLogPartitions( try { await fromDb.execute( sql.raw( - `CREATE TABLE IF NOT EXISTS "${name}" PARTITION OF "exa_usage_log" FOR VALUES FROM ('${format(target, 'yyyy-MM-dd')}') TO ('${format(nextMonth, 'yyyy-MM-dd')}')` + `CREATE TABLE IF NOT EXISTS "public"."${name}" PARTITION OF "public"."exa_usage_log" FOR VALUES FROM ('${format(target, 'yyyy-MM-dd')}') TO ('${format(nextMonth, 'yyyy-MM-dd')}')` ) ); + + // Existing and current partitions require the concurrent operator path. Future + // partitions are write-free, so regular local index creation is deploy-safe. + if (offset > 0) { + const indexDefinitions = buildExaUsageLogPartitionIndexDefinitions('public', name, false); + for (const definition of indexDefinitions) { + await fromDb.execute(sql.raw(definition.statement)); + } + } + created.push(name); } catch (error) { errors.push({ name, error }); diff --git a/apps/web/src/lib/exa-usage.test.ts b/apps/web/src/lib/exa-usage.test.ts index e12f95b367..de7cfcc46e 100644 --- a/apps/web/src/lib/exa-usage.test.ts +++ b/apps/web/src/lib/exa-usage.test.ts @@ -1,10 +1,19 @@ import { describe, test, expect, afterEach } from '@jest/globals'; import { db } from '@/lib/drizzle'; -import { exa_monthly_usage, exa_usage_log, kilocode_users } from '@kilocode/db/schema'; +import { + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, + exa_monthly_usage, + exa_usage_log, + kilocode_users, + organization_user_usage, + organizations, +} from '@kilocode/db/schema'; import { eq, sql } from 'drizzle-orm'; import { insertTestUser } from '@/tests/helpers/user.helper'; import { getExaMonthlyUsage, getExaFreeAllowanceMicrodollars, recordExaUsage } from './exa-usage'; import { EXA_MONTHLY_ALLOWANCE_MICRODOLLARS } from '@/lib/constants'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; // Mock next/server's after function which requires request context jest.mock('next/server', () => ({ @@ -97,6 +106,12 @@ describe('Exa Usage Tracking', () => { expect(rows[0].total_charged_microdollars).toBe(0); expect(rows[0].request_count).toBe(1); expect(rows[0].free_allowance_microdollars).toBe(10_000_000); + + const totals = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, user.id)); + expect(totals).toHaveLength(0); }); test('increments existing counter on subsequent requests', async () => { @@ -215,6 +230,146 @@ describe('Exa Usage Tracking', () => { .where(eq(kilocode_users.id, user.id)); expect(updated.microdollars_used).toBe(7000); + + const [source] = await db + .select() + .from(exa_usage_log) + .where(eq(exa_usage_log.kilo_user_id, user.id)); + const [total] = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, user.id)); + expect(total).toBeDefined(); + const sourceHour = new Date(source.created_at); + sourceHour.setUTCMinutes(0, 0, 0); + expect(new Date(total.hour_start).toISOString()).toBe(sourceHour.toISOString()); + expect(total).toMatchObject({ + owned_by_organization_id: null, + spend_category: 'variable', + total_microdollars: 7000, + spend_record_count: 1, + }); + + const [driver] = await db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, user.id)); + expect(driver).toMatchObject({ + source: 'other', + product_key: 'exa', + feature_key: 'search', + model_or_plan_key: 'other', + provider_key: 'exa', + actor_user_id: user.id, + total_microdollars: 7000, + spend_record_count: 1, + }); + }); + + test('charges organization, tracks member usage, and captures spend atomically', async () => { + const user = await insertTestUser(); + const organization = await createTestOrganization('Exa usage organization', user.id, 50_000); + + await recordExaUsage({ + userId: user.id, + organizationId: organization.id, + path: '/contents', + costMicrodollars: 9000, + chargedToBalance: true, + freeAllowanceMicrodollars: 10_000_000, + }); + + const [chargedOrganization] = await db + .select({ microdollars_used: organizations.microdollars_used }) + .from(organizations) + .where(eq(organizations.id, organization.id)); + expect(chargedOrganization.microdollars_used).toBe(9000); + + const [memberUsage] = await db + .select() + .from(organization_user_usage) + .where(eq(organization_user_usage.organization_id, organization.id)); + expect(memberUsage).toMatchObject({ + kilo_user_id: user.id, + microdollar_usage: 9000, + }); + + const [total] = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_organization_id, organization.id)); + expect(total).toMatchObject({ + owned_by_user_id: null, + spend_category: 'variable', + total_microdollars: 9000, + spend_record_count: 1, + }); + + const [driver] = await db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where( + eq(cost_insight_owner_hour_driver_buckets.owned_by_organization_id, organization.id) + ); + expect(driver).toMatchObject({ + source: 'other', + product_key: 'exa', + feature_key: 'contents', + model_or_plan_key: 'other', + provider_key: 'exa', + actor_user_id: user.id, + total_microdollars: 9000, + }); + }); + + test('maps unsupported Exa paths to the controlled other feature', async () => { + const user = await insertTestUser(); + + await recordExaUsage({ + userId: user.id, + organizationId: undefined, + path: '/unsupported-client-path', + costMicrodollars: 1000, + chargedToBalance: true, + freeAllowanceMicrodollars: 10_000_000, + }); + + const [driver] = await db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, user.id)); + expect(driver.feature_key).toBe('other'); + }); + + test('rolls back Exa source and monthly rows when mandatory capture fails', async () => { + const missingUserId = `missing-exa-user-${crypto.randomUUID()}`; + + await expect( + recordExaUsage({ + userId: missingUserId, + organizationId: undefined, + path: '/search', + costMicrodollars: 1000, + chargedToBalance: true, + freeAllowanceMicrodollars: 10_000_000, + }) + ).rejects.toThrow(); + + const sourceRows = await db + .select() + .from(exa_usage_log) + .where(eq(exa_usage_log.kilo_user_id, missingUserId)); + const monthlyRows = await db + .select() + .from(exa_monthly_usage) + .where(eq(exa_monthly_usage.kilo_user_id, missingUserId)); + const totals = await db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, missingUserId)); + expect(sourceRows).toHaveLength(0); + expect(monthlyRows).toHaveLength(0); + expect(totals).toHaveLength(0); }); test('does not deduct from balance when chargedToBalance is false', async () => { diff --git a/apps/web/src/lib/exa-usage.ts b/apps/web/src/lib/exa-usage.ts index 23a135c245..cb80c253e7 100644 --- a/apps/web/src/lib/exa-usage.ts +++ b/apps/web/src/lib/exa-usage.ts @@ -1,15 +1,19 @@ -import { db } from '@/lib/drizzle'; -import { - exa_monthly_usage, - exa_usage_log, - kilocode_users, - type MicrodollarUsage, - type User, -} from '@kilocode/db/schema'; -import { ABUSE_CLASSIFICATION } from '@kilocode/db/schema-types'; +import { randomUUID } from 'node:crypto'; +import { db, type DrizzleTransaction } from '@/lib/drizzle'; +import { exa_monthly_usage, exa_usage_log, kilocode_users, type User } from '@kilocode/db/schema'; import { eq, sql } from 'drizzle-orm'; -import { ingestOrganizationTokenUsage } from '@/lib/organizations/organization-usage'; +import { + mutateOrganizationUsage, + scheduleOrganizationLowBalanceAlert, +} from '@/lib/organizations/organization-usage'; +import type { OrganizationUsageMutationResult } from '@/lib/organizations/organization-usage'; import { EXA_MONTHLY_ALLOWANCE_MICRODOLLARS } from '@/lib/constants'; +import { + captureCostInsightSpend, + COST_INSIGHT_DRIVER_FALLBACK, + COST_INSIGHT_EXA_PRODUCT_KEY, +} from '@kilocode/db/cost-insights-rollups'; +import { getExaCostInsightFeatureKey } from '@/lib/exa-paths'; export type ExaMonthlyUsageResult = { /** Total spend in microdollars for the current month. */ @@ -61,10 +65,8 @@ export async function getExaMonthlyUsage( } /** - * Records a single Exa request: - * 1. Upserts exa_monthly_usage counter (atomic increment). - * 2. Appends to exa_usage_log (audit trail). - * 3. If chargedToBalance, deducts from the user's (or org's) Kilo credit balance. + * Records the source row, monthly counter, charged owner mutation, and Cost Insights + * rollup atomically. Low-balance notification scheduling happens only after commit. */ export async function recordExaUsage(params: { userId: string; @@ -87,38 +89,60 @@ export async function recordExaUsage(params: { type, } = params; const chargedAmount = chargedToBalance ? costMicrodollars : 0; + const sourceId = randomUUID(); + const occurredAt = new Date().toISOString(); - // 1. Append to the usage log first. This is the source of truth for balance - // recomputation, so it must succeed before we touch any counters. If this - // fails (e.g. missing partition), nothing else is modified and recompute - // can still reconcile from the log rows that do exist. - await db.insert(exa_usage_log).values({ - kilo_user_id: userId, - organization_id: organizationId ?? null, - path, - cost_microdollars: costMicrodollars, - charged_to_balance: chargedToBalance, - feature_id: featureId ?? null, - type: type ?? null, - }); + const organizationUsage = await db.transaction(async tx => { + await tx.insert(exa_usage_log).values({ + id: sourceId, + kilo_user_id: userId, + organization_id: organizationId ?? null, + path, + cost_microdollars: costMicrodollars, + charged_to_balance: chargedToBalance, + feature_id: featureId ?? null, + type: type ?? null, + created_at: occurredAt, + }); - // 2. Upsert the monthly counter (atomic increment). - // free_allowance_microdollars is set on INSERT (first request of the month) - // but NOT updated on conflict — the first-of-month value is locked in. - // Two partial unique indexes exist: one for personal (org IS NULL) and one - // for org usage (org IS NOT NULL), so the upsert must target the right one. - await upsertMonthlyCounter({ - userId, - organizationId, - costMicrodollars, - chargedAmount, - freeAllowanceMicrodollars, + await upsertMonthlyCounter(tx, { + userId, + organizationId, + occurredAt, + costMicrodollars, + chargedAmount, + freeAllowanceMicrodollars, + }); + + if (!chargedToBalance || costMicrodollars <= 0) return null; + + const result = await deductFromBalance(tx, { + userId, + organizationId, + occurredAt, + costMicrodollars, + }); + + await captureCostInsightSpend(tx, { + owner: organizationId + ? { type: 'organization', id: organizationId } + : { type: 'user', id: userId }, + actorUserId: userId, + occurredAt, + amountMicrodollars: costMicrodollars, + category: 'variable', + source: 'other', + productKey: COST_INSIGHT_EXA_PRODUCT_KEY, + featureKey: getExaCostInsightFeatureKey(path), + modelOrPlanKey: COST_INSIGHT_DRIVER_FALLBACK, + providerKey: COST_INSIGHT_EXA_PRODUCT_KEY, + }); + + return result; }); - // 3. If over the free tier, deduct from the Kilo credit balance. - // If this fails, the log row exists so recompute can recover. - if (chargedToBalance && costMicrodollars > 0) { - await deductFromBalance(userId, organizationId, costMicrodollars, path); + if (organizationId && organizationUsage) { + scheduleOrganizationLowBalanceAlert(organizationId, organizationUsage); } } @@ -126,15 +150,25 @@ export async function recordExaUsage(params: { * Upserts the monthly counter row, targeting the correct partial unique index * based on whether the request is personal (no org) or org-scoped. */ -async function upsertMonthlyCounter(params: { - userId: string; - organizationId: string | undefined; - costMicrodollars: number; - chargedAmount: number; - freeAllowanceMicrodollars: number; -}): Promise { - const { userId, organizationId, costMicrodollars, chargedAmount, freeAllowanceMicrodollars } = - params; +async function upsertMonthlyCounter( + tx: DrizzleTransaction, + params: { + userId: string; + organizationId: string | undefined; + occurredAt: string; + costMicrodollars: number; + chargedAmount: number; + freeAllowanceMicrodollars: number; + } +): Promise { + const { + userId, + organizationId, + occurredAt, + costMicrodollars, + chargedAmount, + freeAllowanceMicrodollars, + } = params; const doUpdateSet = sql` total_cost_microdollars = ${exa_monthly_usage.total_cost_microdollars} + ${costMicrodollars}, @@ -144,12 +178,12 @@ async function upsertMonthlyCounter(params: { `; if (organizationId) { - await db.execute(sql` + await tx.execute(sql` INSERT INTO ${exa_monthly_usage} ( kilo_user_id, organization_id, month, total_cost_microdollars, total_charged_microdollars, request_count, free_allowance_microdollars ) VALUES ( - ${userId}, ${organizationId}, date_trunc('month', now())::date, + ${userId}, ${organizationId}, date_trunc('month', ${occurredAt}::timestamptz AT TIME ZONE 'UTC')::date, ${costMicrodollars}, ${chargedAmount}, 1, ${freeAllowanceMicrodollars} ) ON CONFLICT (kilo_user_id, organization_id, month) @@ -157,12 +191,12 @@ async function upsertMonthlyCounter(params: { DO UPDATE SET ${doUpdateSet} `); } else { - await db.execute(sql` + await tx.execute(sql` INSERT INTO ${exa_monthly_usage} ( kilo_user_id, month, total_cost_microdollars, total_charged_microdollars, request_count, free_allowance_microdollars ) VALUES ( - ${userId}, date_trunc('month', now())::date, + ${userId}, date_trunc('month', ${occurredAt}::timestamptz AT TIME ZONE 'UTC')::date, ${costMicrodollars}, ${chargedAmount}, 1, ${freeAllowanceMicrodollars} ) ON CONFLICT (kilo_user_id, month) @@ -172,51 +206,30 @@ async function upsertMonthlyCounter(params: { } } -/** - * Deducts Exa overage cost from the user's personal balance or their org's balance. - * Personal: increments kilocode_users.microdollars_used. - * Org: delegates to ingestOrganizationTokenUsage which handles org balance + daily limits + alerts. - */ async function deductFromBalance( - userId: string, - organizationId: string | undefined, - costMicrodollars: number, - path: string -): Promise { + tx: DrizzleTransaction, + params: { + userId: string; + organizationId: string | undefined; + occurredAt: string; + costMicrodollars: number; + } +): Promise { + const { userId, organizationId, occurredAt, costMicrodollars } = params; if (organizationId) { - // Org billing: reuse the existing org billing pipeline which handles - // balance updates, per-user daily tracking, and low-balance alerts. - const usageRecord = { - id: crypto.randomUUID(), + return mutateOrganizationUsage(tx, { kilo_user_id: userId, - cost: costMicrodollars, organization_id: organizationId, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: new Date().toISOString(), - provider: 'exa', - model: path, - requested_model: null, - cache_discount: null, - has_error: false, - abuse_classification: ABUSE_CLASSIFICATION.NOT_CLASSIFIED, - inference_provider: null, - project_id: null, - } satisfies MicrodollarUsage; - - await ingestOrganizationTokenUsage(usageRecord); - } else { - // Personal billing: directly increment the user's usage counter. - // WARNING: Do NOT also insert into microdollar_usage here. Recompute - // (recomputeUserBalances) already picks up personal Exa charges from - // exa_usage_log. Adding a microdollar_usage row would double-count. - await db - .update(kilocode_users) - .set({ - microdollars_used: sql`${kilocode_users.microdollars_used} + ${costMicrodollars}`, - }) - .where(eq(kilocode_users.id, userId)); + cost: costMicrodollars, + created_at: occurredAt, + }); } + + await tx + .update(kilocode_users) + .set({ + microdollars_used: sql`${kilocode_users.microdollars_used} + ${costMicrodollars}`, + }) + .where(eq(kilocode_users.id, userId)); + return null; } diff --git a/apps/web/src/lib/kiloclaw/credit-billing.ts b/apps/web/src/lib/kiloclaw/credit-billing.ts index 1fc9335ed9..d4836cf648 100644 --- a/apps/web/src/lib/kiloclaw/credit-billing.ts +++ b/apps/web/src/lib/kiloclaw/credit-billing.ts @@ -14,6 +14,11 @@ import { type KiloClawSubscriptionChangeAction, type KiloClawSubscriptionChangeActor, } from '@kilocode/db'; +import { + captureCostInsightSpend, + COST_INSIGHT_DRIVER_FALLBACK, + COST_INSIGHT_KILOCLAW_PRODUCT_KEY, +} from '@kilocode/db/cost-insights-rollups'; import { credit_transactions, kilocode_users, @@ -1519,6 +1524,7 @@ export async function enrollWithCredits(params: { credit_category: deductionCategory, check_category_uniqueness: true, original_baseline_microdollars_used: user.microdollars_used, + created_at: periodStartIso, }) .onConflictDoNothing(); @@ -1535,6 +1541,19 @@ export async function enrollWithCredits(params: { return; } + await captureCostInsightSpend(tx, { + owner: { type: 'user', id: userId }, + actorUserId: userId, + occurredAt: periodStartIso, + amountMicrodollars: costMicrodollars, + category: 'scheduled', + source: 'kiloclaw', + productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, + featureKey: 'enrollment', + modelOrPlanKey: plan, + providerKey: COST_INSIGHT_DRIVER_FALLBACK, + }); + if ( currentSubscription?.status === 'canceled' && currentSubscription.kiloclaw_price_version !== kiloclawPriceVersion diff --git a/apps/web/src/lib/organizations/organization-usage.test.ts b/apps/web/src/lib/organizations/organization-usage.test.ts index 334929d707..801b81b25d 100644 --- a/apps/web/src/lib/organizations/organization-usage.test.ts +++ b/apps/web/src/lib/organizations/organization-usage.test.ts @@ -1,4 +1,6 @@ import { describe, test, expect, afterEach } from '@jest/globals'; +import { after } from 'next/server'; +import { sendBalanceAlertEmail } from '@/lib/email'; import { db } from '@/lib/drizzle'; import { organizations, organization_user_usage } from '@kilocode/db/schema'; import { insertTestUser } from '@/tests/helpers/user.helper'; @@ -13,10 +15,17 @@ import { import { getBalanceForOrganizationUser, ingestOrganizationTokenUsage, + mutateOrganizationUsage, + scheduleOrganizationLowBalanceAlert, updateOrganizationUserLimit, } from './organization-usage'; import { createOrganizationUsage } from '@/tests/helpers/microdollar-usage.helper'; +jest.mock('@/lib/email', () => ({ + ...jest.requireActual('@/lib/email'), + sendBalanceAlertEmail: jest.fn().mockResolvedValue(undefined), +})); + // Mock next/server's after function which requires request context jest.mock('next/server', () => { return { @@ -30,6 +39,7 @@ jest.mock('next/server', () => { describe('Organization Usage Functions', () => { afterEach(async () => { + jest.clearAllMocks(); // eslint-disable-next-line drizzle/enforce-delete-with-where await db.delete(organizations); }); @@ -167,6 +177,35 @@ describe('Organization Usage Functions', () => { expect(result.balance).toBe(0.04); // 40000 microdollars = 0.04 USD (unchanged) }); + test('separates low-balance mutation from exactly-once post-commit scheduling', async () => { + const user = await insertTestUser(); + const organization = await createTestOrganization('Balance Alert Org', user.id, 50_000, { + minimum_balance: 0.04, + minimum_balance_alert_email: ['billing@example.com'], + }); + const usage = await createOrganizationUsage(20_000, user.id, organization.id); + + const result = await db.transaction(tx => mutateOrganizationUsage(tx, usage)); + + expect(result).toEqual({ + crossedMinimumBalance: true, + recipients: ['billing@example.com'], + minimumBalanceMicrodollars: 40_000, + }); + expect(jest.mocked(after)).not.toHaveBeenCalled(); + expect(jest.mocked(sendBalanceAlertEmail)).not.toHaveBeenCalled(); + + scheduleOrganizationLowBalanceAlert(organization.id, result); + + expect(jest.mocked(after)).toHaveBeenCalledTimes(1); + expect(jest.mocked(sendBalanceAlertEmail)).toHaveBeenCalledTimes(1); + expect(jest.mocked(sendBalanceAlertEmail)).toHaveBeenCalledWith({ + organizationId: organization.id, + minimum_balance: 0.04, + to: ['billing@example.com'], + }); + }); + test('should handle large cost usage', async () => { const user = await insertTestUser(); const organization = await createTestOrganization('Test Org', user.id, 1000000); diff --git a/apps/web/src/lib/organizations/organization-usage.ts b/apps/web/src/lib/organizations/organization-usage.ts index b776eebe2b..f8f4ead953 100644 --- a/apps/web/src/lib/organizations/organization-usage.ts +++ b/apps/web/src/lib/organizations/organization-usage.ts @@ -206,85 +206,125 @@ export async function getBalanceForOrganizationUser( return { balance: fromMicrodollars(cappedBalance), settings, plan }; } -export async function ingestOrganizationTokenUsage(usage: MicrodollarUsage): Promise { - const { cost, kilo_user_id, organization_id } = usage; - - if (!organization_id) return; - return await db.transaction(async tx => { - // Get current balance and settings before the update - const [orgData] = await tx - .select({ - total_microdollars_acquired: organizations.total_microdollars_acquired, - microdollars_used: organizations.microdollars_used, - settings: organizations.settings, - }) - .from(organizations) - .where(eq(organizations.id, organization_id)) - .limit(1); +export type OrganizationUsageMutationResult = { + crossedMinimumBalance: boolean; + recipients: string[]; + minimumBalanceMicrodollars: number | null; +}; + +const NO_ORGANIZATION_BALANCE_ALERT: OrganizationUsageMutationResult = { + crossedMinimumBalance: false, + recipients: [], + minimumBalanceMicrodollars: null, +}; + +export type OrganizationUsageMutationInput = Pick< + MicrodollarUsage, + 'kilo_user_id' | 'organization_id' | 'cost' | 'created_at' +>; - const currentBalance = - (orgData?.total_microdollars_acquired ?? 0) - (orgData?.microdollars_used ?? 0); +export async function mutateOrganizationUsage( + tx: DrizzleTransaction, + usage: OrganizationUsageMutationInput +): Promise { + const { cost, kilo_user_id, organization_id, created_at } = usage; + if (!organization_id) return NO_ORGANIZATION_BALANCE_ALERT; - const minimumBalance = orgData?.settings?.minimum_balance - ? toMicrodollars(orgData?.settings?.minimum_balance) - : null; + const [orgData] = await tx + .select({ + total_microdollars_acquired: organizations.total_microdollars_acquired, + microdollars_used: organizations.microdollars_used, + settings: organizations.settings, + }) + .from(organizations) + .where(eq(organizations.id, organization_id)) + .limit(1) + .for('update'); + + if (!orgData) return NO_ORGANIZATION_BALANCE_ALERT; + + const currentBalance = orgData.total_microdollars_acquired - orgData.microdollars_used; + const minimumBalance = orgData.settings?.minimum_balance + ? toMicrodollars(orgData.settings.minimum_balance) + : null; + const newBalance = currentBalance - cost; + const crossedMinimumBalance = + minimumBalance != null && currentBalance >= minimumBalance && newBalance < minimumBalance; + const recipients = crossedMinimumBalance + ? (orgData.settings?.minimum_balance_alert_email ?? []) + : []; + + await tx + .update(organizations) + .set({ + microdollars_used: sql`${organizations.microdollars_used} + ${cost}`, + microdollars_balance: sql`${organizations.microdollars_balance} - ${cost}`, + }) + .where(eq(organizations.id, organization_id)); + + const limitType: OrganizationUserLimitType = 'daily'; + await tx.execute(sql` + INSERT INTO ${organization_user_usage} ( + organization_id, + kilo_user_id, + usage_date, + limit_type, + microdollar_usage, + created_at, + updated_at + ) + SELECT + ${organization_id}, + ${kilo_user_id}, + (${created_at}::timestamptz AT TIME ZONE 'UTC')::date, + ${limitType}, + ${cost}, + NOW(), + NOW() + WHERE EXISTS ( + SELECT 1 + FROM ${organization_memberships} + WHERE ${organization_memberships.organization_id} = ${organization_id} + AND ${organization_memberships.kilo_user_id} = ${kilo_user_id} + ) + ON CONFLICT (organization_id, kilo_user_id, limit_type, usage_date) + DO UPDATE SET + microdollar_usage = ${organization_user_usage.microdollar_usage} + ${cost}, + updated_at = NOW() + `); + + return { + crossedMinimumBalance, + recipients, + minimumBalanceMicrodollars: minimumBalance, + }; +} - const newBalance = currentBalance - cost; +export function scheduleOrganizationLowBalanceAlert( + organizationId: Organization['id'], + result: OrganizationUsageMutationResult +): void { + const { crossedMinimumBalance, minimumBalanceMicrodollars, recipients } = result; + if (!crossedMinimumBalance || minimumBalanceMicrodollars == null) return; - // Check if balance is crossing the minimum_balance threshold - if (minimumBalance != null && currentBalance >= minimumBalance && newBalance < minimumBalance) { - const alertEmails = orgData?.settings?.minimum_balance_alert_email ?? []; - logExceptInTest( - `[ingestOrganizationTokenUsage] Balance alert triggered for org ${organization_id}: currentBalance=${fromMicrodollars(currentBalance)} newBalance=${fromMicrodollars(newBalance)} threshold=${fromMicrodollars(minimumBalance)} recipients=${alertEmails.length}` - ); - // Send email notification about low balance (don't block the transaction, but do make Vercel wait on the Promise before shutting down) - after( - sendBalanceAlertEmail({ - organizationId: organization_id, - minimum_balance: fromMicrodollars(minimumBalance), - to: alertEmails, - }).catch(err => { - console.error('[ingestOrganizationTokenUsage] Failed to send balance alert email:', err); - }) - ); - } + logExceptInTest( + `[ingestOrganizationTokenUsage] Balance alert triggered for org ${organizationId}: threshold=${fromMicrodollars(minimumBalanceMicrodollars)} recipients=${recipients.length}` + ); + after(() => + sendBalanceAlertEmail({ + organizationId, + minimum_balance: fromMicrodollars(minimumBalanceMicrodollars), + to: recipients, + }).catch(err => { + console.error('[ingestOrganizationTokenUsage] Failed to send balance alert email:', err); + }) + ); +} - // Update organization usage (always happens regardless of membership) - await tx - .update(organizations) - .set({ - microdollars_used: sql`${organizations.microdollars_used} + ${cost}`, - microdollars_balance: sql`${organizations.microdollars_balance} - ${cost}`, - }) - .where(eq(organizations.id, organization_id)); - - const limitType: OrganizationUserLimitType = 'daily'; - // Track user usage only if they are a member of the organization - // Use INSERT with a subquery that only inserts if the user is a member - await tx.execute(sql` - INSERT INTO ${organization_user_usage} ( - organization_id, - kilo_user_id, - usage_date, - limit_type, - microdollar_usage, - created_at, - updated_at - ) - SELECT - ${organization_id}, - ${kilo_user_id}, - CURRENT_DATE, - ${limitType}, - ${cost}, - NOW(), - NOW() - ON CONFLICT (organization_id, kilo_user_id, limit_type, usage_date) - DO UPDATE SET - microdollar_usage = ${organization_user_usage.microdollar_usage} + ${cost}, - updated_at = NOW() - `); - }); +export async function ingestOrganizationTokenUsage(usage: MicrodollarUsage): Promise { + if (!usage.organization_id) return; + const result = await db.transaction(tx => mutateOrganizationUsage(tx, usage)); + scheduleOrganizationLowBalanceAlert(usage.organization_id, result); } const MAX_DAILY_LIMIT_USD = 2000; diff --git a/apps/web/src/routers/kiloclaw-billing-router.test.ts b/apps/web/src/routers/kiloclaw-billing-router.test.ts index c1690a3b8a..efa9e69856 100644 --- a/apps/web/src/routers/kiloclaw-billing-router.test.ts +++ b/apps/web/src/routers/kiloclaw-billing-router.test.ts @@ -37,6 +37,7 @@ import type Stripe from 'stripe'; import { KiloPassTier, KiloPassCadence, KiloPassPaymentProvider } from '@/lib/kilo-pass/enums'; import { differenceInCalendarMonths } from 'date-fns'; import { CURRENT_KILOCLAW_PRICE_VERSION, LEGACY_KILOCLAW_PRICE_VERSION } from '@kilocode/db'; +import type { CaptureCostInsightSpendInput } from '@kilocode/db/cost-insights-rollups'; (kiloclaw_subscriptions.kiloclaw_price_version as { defaultFn: () => string }).defaultFn = () => LEGACY_KILOCLAW_PRICE_VERSION; @@ -53,6 +54,18 @@ jest.setTimeout(15_000); // ── Mocks ────────────────────────────────────────────────────────────────── +jest.mock('@kilocode/db/cost-insights-rollups', () => ({ + captureCostInsightSpend: jest.fn(async () => undefined), + COST_INSIGHT_DRIVER_FALLBACK: 'other', + COST_INSIGHT_KILOCLAW_PRODUCT_KEY: 'kiloclaw-hosting', +})); + +const captureCostInsightSpendMock = jest.requireMock<{ + captureCostInsightSpend: jest.Mock< + (tx: unknown, input: CaptureCostInsightSpendInput) => Promise + >; +}>('@kilocode/db/cost-insights-rollups').captureCostInsightSpend; + jest.mock('@/lib/stripe-client', () => { // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const { errors } = require('stripe').default ?? require('stripe'); @@ -278,6 +291,7 @@ beforeEach(async () => { kiloclawInternalClientMock.__provisionMock.mockResolvedValue(defaultProvisionResult); kiloclawInternalClientMock.__startAsyncMock.mockReset(); kiloclawInternalClientMock.__startAsyncMock.mockResolvedValue(undefined); + captureCostInsightSpendMock.mockClear(); posthogCaptureMock.mockReset(); // Default mock returns for live-fetch calls @@ -6969,6 +6983,22 @@ describe('enrollWithCredits', () => { expect(deduction).toBeDefined(); expect(deduction!.amount_microdollars).toBe(-4_000_000); expect(deduction!.credit_category).toContain('kiloclaw-subscription:'); + expect(captureCostInsightSpendMock).toHaveBeenCalledWith(expect.anything(), { + owner: { type: 'user', id: user.id }, + actorUserId: user.id, + occurredAt: expect.any(String), + amountMicrodollars: 4_000_000, + category: 'scheduled', + source: 'kiloclaw', + productKey: 'kiloclaw-hosting', + featureKey: 'enrollment', + modelOrPlanKey: 'standard', + providerKey: 'other', + }); + const captureInput = captureCostInsightSpendMock.mock.calls[0]?.[1] as + | { occurredAt: string } + | undefined; + expect(new Date(deduction!.created_at).toISOString()).toBe(captureInput?.occurredAt); // Verify credit spend recorded at intro amount const [updatedUser] = await db @@ -7274,6 +7304,36 @@ describe('enrollWithCredits', () => { ); }); + it('rolls back credit enrollment when scheduled-spend capture fails', async () => { + await createCreditEnrollmentAnchor(user.id); + await giveUserCredits(user.id, 50_000_000); + captureCostInsightSpendMock.mockImplementationOnce(async () => { + throw new Error('rollup unavailable'); + }); + + const caller = await createCallerForUser(user.id); + await expect(caller.kiloclaw.enrollWithCredits({ plan: 'standard' })).rejects.toThrow( + 'rollup unavailable' + ); + + const [subscription] = await db + .select() + .from(kiloclaw_subscriptions) + .where(eq(kiloclaw_subscriptions.user_id, user.id)); + const [updatedUser] = await db + .select({ used: kilocode_users.microdollars_used }) + .from(kilocode_users) + .where(eq(kilocode_users.id, user.id)); + const transactions = await db + .select() + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); + + expect(subscription.status).toBe('trialing'); + expect(updatedUser.used).toBe(0); + expect(transactions).toHaveLength(0); + }); + it('rejects enrollment when subscription is active', async () => { const instance = await createInstance(user.id); await giveUserCredits(user.id, 50_000_000); @@ -7747,6 +7807,7 @@ describe('enrollWithCredits', () => { await expect(caller.kiloclaw.enrollWithCredits({ plan: 'standard' })).rejects.toThrow( 'Enrollment already processed for this billing period' ); + expect(captureCostInsightSpendMock).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/web/src/scripts/db/cost-insights-rollups.ts b/apps/web/src/scripts/db/cost-insights-rollups.ts new file mode 100644 index 0000000000..449e1c776e --- /dev/null +++ b/apps/web/src/scripts/db/cost-insights-rollups.ts @@ -0,0 +1,183 @@ +import { db } from '@/lib/drizzle'; +import { + backfillCostInsightRollupsNewestFirst, + initializeCostInsightRollupCoverage, + reconcileCostInsightRollups, + recordCostInsightDegradedInterval, + recordCostInsightReconciliationSuccess, +} from '@/lib/cost-insights/rollup-maintenance'; +import { requireUtcHour } from '@/lib/cost-insights/canonical-sources'; + +const HOUR_MS = 60 * 60 * 1_000; + +export type CostInsightRollupScriptArgs = { + execute: boolean; + startHour: string; + endHourExclusive: string; + maxHours: number; + sleepMs: number; + liveCaptureStartHour?: string; +}; + +function usage(): string { + return [ + 'Usage:', + ' pnpm --filter web script:run db cost-insights-rollups --start-hour --end-hour --max-hours [--sleep-ms ] [--execute] [--live-capture-start-hour ]', + '', + 'Defaults to dry-run reconciliation. --execute performs absolute newest-first hourly replacement.', + 'First execution must set --live-capture-start-hour to the first full UTC hour after all writers were deployed.', + 'Both bounds must be exact UTC hours; --end-hour is exclusive and cannot exceed current completed-hour boundary.', + ].join('\n'); +} + +function parseNonNegativeInteger(value: string | undefined, flag: string): number { + if (!value || !/^\d+$/.test(value)) { + throw new Error(`${flag} must be a non-negative integer.\n${usage()}`); + } + const parsed = Number(value); + if (!Number.isSafeInteger(parsed)) { + throw new Error(`${flag} exceeds the JavaScript safe-integer range.\n${usage()}`); + } + return parsed; +} + +export function parseCostInsightRollupScriptArgs(args: string[]): CostInsightRollupScriptArgs { + let execute = false; + let startHour: string | undefined; + let endHourExclusive: string | undefined; + let maxHours: number | undefined; + let sleepMs = 0; + let liveCaptureStartHour: string | undefined; + const seen = new Set(); + + for (let index = 0; index < args.length; index++) { + const flag = args[index]; + if (seen.has(flag)) { + throw new Error(`Duplicate flag: ${flag}.\n${usage()}`); + } + seen.add(flag); + if (flag === '--execute') { + execute = true; + continue; + } + const value = args[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for ${flag}.\n${usage()}`); + } + index++; + if (flag === '--start-hour') { + startHour = requireUtcHour(value, '--start-hour'); + } else if (flag === '--end-hour') { + endHourExclusive = requireUtcHour(value, '--end-hour'); + } else if (flag === '--max-hours') { + maxHours = parseNonNegativeInteger(value, '--max-hours'); + } else if (flag === '--sleep-ms') { + sleepMs = parseNonNegativeInteger(value, '--sleep-ms'); + } else if (flag === '--live-capture-start-hour') { + liveCaptureStartHour = requireUtcHour(value, '--live-capture-start-hour'); + } else { + throw new Error(`Unknown flag: ${flag}.\n${usage()}`); + } + } + + if (!startHour || !endHourExclusive || maxHours === undefined || maxHours === 0) { + throw new Error(usage()); + } + const hourCount = (Date.parse(endHourExclusive) - Date.parse(startHour)) / HOUR_MS; + if (!Number.isInteger(hourCount) || hourCount <= 0 || hourCount > maxHours) { + throw new Error(`Requested range must contain 1-${maxHours} UTC hours.\n${usage()}`); + } + const currentCompletedHourBoundary = Math.floor(Date.now() / HOUR_MS) * HOUR_MS; + if (Date.parse(endHourExclusive) > currentCompletedHourBoundary) { + throw new Error(`--end-hour must not include current or future UTC hours.\n${usage()}`); + } + if (liveCaptureStartHour && !execute) { + throw new Error(`--live-capture-start-hour requires --execute.\n${usage()}`); + } + + return { + execute, + startHour, + endHourExclusive, + maxHours, + sleepMs, + ...(liveCaptureStartHour ? { liveCaptureStartHour } : {}), + }; +} + +function printReconciliation( + report: Awaited>, + mode: 'dry-run' | 'post-execute-reconciliation' +): void { + console.log( + JSON.stringify( + { + mode, + startHour: report.startHour, + endHourExclusive: report.endHourExclusive, + checkedHourCount: report.checkedHourCount, + mismatchCount: report.mismatchCount, + mismatchCounts: report.mismatchCounts, + detailsTruncated: report.detailsTruncated, + mismatches: report.mismatches, + }, + null, + 2 + ) + ); +} + +export async function run(...args: string[]): Promise { + const parsed = parseCostInsightRollupScriptArgs(args); + if (!parsed.execute) { + const report = await reconcileCostInsightRollups(db, { + startHour: parsed.startHour, + endHourExclusive: parsed.endHourExclusive, + maxHours: parsed.maxHours, + }); + printReconciliation(report, 'dry-run'); + return; + } + + if (parsed.liveCaptureStartHour) { + await initializeCostInsightRollupCoverage(db, parsed.liveCaptureStartHour); + } + console.log( + JSON.stringify({ + mode: 'execute', + order: 'newest-first', + startHour: parsed.startHour, + endHourExclusive: parsed.endHourExclusive, + maxHours: parsed.maxHours, + sleepMs: parsed.sleepMs, + liveCaptureStartHour: parsed.liveCaptureStartHour ?? null, + }) + ); + await backfillCostInsightRollupsNewestFirst(db, { + startHour: parsed.startHour, + endHourExclusive: parsed.endHourExclusive, + maxHours: parsed.maxHours, + sleepMs: parsed.sleepMs, + onHourComplete: result => { + console.log(JSON.stringify({ mode: 'execute-hour', ...result })); + }, + }); + + const report = await reconcileCostInsightRollups(db, { + startHour: parsed.startHour, + endHourExclusive: parsed.endHourExclusive, + maxHours: parsed.maxHours, + }); + printReconciliation(report, 'post-execute-reconciliation'); + if (report.mismatchCount > 0) { + const degradedIntervalId = await recordCostInsightDegradedInterval(db, { + startHour: parsed.startHour, + endHourExclusive: parsed.endHourExclusive, + reason: 'reconciliation_mismatch', + }); + throw new Error( + `Cost Insights reconciliation found mismatches; degraded interval ${degradedIntervalId} remains unresolved.` + ); + } + await recordCostInsightReconciliationSuccess(db); +} diff --git a/apps/web/src/scripts/db/exa-usage-log-indexes.ts b/apps/web/src/scripts/db/exa-usage-log-indexes.ts new file mode 100644 index 0000000000..ba8d2c8a85 --- /dev/null +++ b/apps/web/src/scripts/db/exa-usage-log-indexes.ts @@ -0,0 +1,169 @@ +import { setTimeout as sleep } from 'node:timers/promises'; + +import { db, type db as defaultDb } from '@/lib/drizzle'; +import { buildExaUsageLogPartitionIndexDefinitions } from '@/lib/exa-usage-partitions'; +import { sql } from 'drizzle-orm'; + +export type ExaUsageLogIndexScriptArgs = { + execute: boolean; + maxPartitions?: number; + sleepMs: number; +}; + +type ExaUsageLogIndexDb = Pick; + +type ExaUsageLogPartitionCatalogRow = { + schema_name: string; + partition_name: string; +}; + +function usage(): string { + return [ + 'Usage:', + ' pnpm --filter web script:run db exa-usage-log-indexes [--max-partitions ] [--sleep-ms ] [--execute]', + '', + 'Defaults to dry-run. --execute creates both local partial indexes on each selected partition.', + 'Partitions are read newest-first from PostgreSQL catalogs. --max-partitions bounds one run; --sleep-ms paces partitions.', + ].join('\n'); +} + +function parseInteger(value: string | undefined, flag: string, allowZero: boolean): number { + if (!value || !/^\d+$/.test(value)) { + throw new Error( + `${flag} must be ${allowZero ? 'a non-negative' : 'a positive'} integer.\n${usage()}` + ); + } + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || (!allowZero && parsed === 0)) { + throw new Error( + `${flag} must be ${allowZero ? 'a non-negative' : 'a positive'} safe integer.\n${usage()}` + ); + } + return parsed; +} + +export function parseExaUsageLogIndexScriptArgs(args: string[]): ExaUsageLogIndexScriptArgs { + let execute = false; + let maxPartitions: number | undefined; + let sleepMs = 0; + const seen = new Set(); + + for (let index = 0; index < args.length; index++) { + const flag = args[index]; + if (seen.has(flag)) { + throw new Error(`Duplicate flag: ${flag}.\n${usage()}`); + } + seen.add(flag); + + if (flag === '--execute') { + execute = true; + continue; + } + if (flag !== '--max-partitions' && flag !== '--sleep-ms') { + throw new Error(`Unknown flag: ${flag}.\n${usage()}`); + } + + const value = args[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for ${flag}.\n${usage()}`); + } + index++; + + if (flag === '--max-partitions') { + maxPartitions = parseInteger(value, flag, false); + } else { + sleepMs = parseInteger(value, flag, true); + } + } + + return { + execute, + ...(maxPartitions === undefined ? {} : { maxPartitions }), + sleepMs, + }; +} + +export async function listExaUsageLogPartitions( + fromDb: ExaUsageLogIndexDb +): Promise { + const result = await fromDb.execute(sql` + SELECT + partition_namespace.nspname AS schema_name, + partition_class.relname AS partition_name + FROM pg_catalog.pg_partition_tree( + pg_catalog.to_regclass('public.exa_usage_log') + ) AS partition_tree + INNER JOIN pg_catalog.pg_class AS partition_class + ON partition_class.oid = partition_tree.relid + INNER JOIN pg_catalog.pg_namespace AS partition_namespace + ON partition_namespace.oid = partition_class.relnamespace + WHERE partition_tree.level > 0 + AND partition_tree.isleaf + AND partition_class.relispartition + ORDER BY partition_class.relname DESC + `); + + return result.rows; +} + +export async function provisionHistoricalExaUsageLogIndexes( + fromDb: ExaUsageLogIndexDb, + options: ExaUsageLogIndexScriptArgs +): Promise { + const partitions = await listExaUsageLogPartitions(fromDb); + const selectedPartitions = + options.maxPartitions === undefined ? partitions : partitions.slice(0, options.maxPartitions); + const plans = selectedPartitions.map(partition => ({ + ...partition, + indexes: buildExaUsageLogPartitionIndexDefinitions( + partition.schema_name, + partition.partition_name, + true + ), + })); + + console.log( + JSON.stringify({ + mode: options.execute ? 'execute' : 'dry-run', + discoveredPartitionCount: partitions.length, + selectedPartitionCount: plans.length, + maxPartitions: options.maxPartitions ?? null, + sleepMs: options.sleepMs, + }) + ); + + for (let partitionIndex = 0; partitionIndex < plans.length; partitionIndex++) { + const plan = plans[partitionIndex]; + if (!options.execute) { + console.log( + JSON.stringify({ + mode: 'dry-run-partition', + schemaName: plan.schema_name, + partitionName: plan.partition_name, + indexes: plan.indexes, + }) + ); + continue; + } + + for (const index of plan.indexes) { + console.log( + JSON.stringify({ + mode: 'execute-index', + schemaName: plan.schema_name, + partitionName: plan.partition_name, + indexName: index.name, + }) + ); + await fromDb.execute(sql.raw(index.statement)); + } + + if (options.sleepMs > 0 && partitionIndex < plans.length - 1) { + await sleep(options.sleepMs); + } + } +} + +export async function run(...args: string[]): Promise { + await provisionHistoricalExaUsageLogIndexes(db, parseExaUsageLogIndexScriptArgs(args)); +} diff --git a/dev/seed/cost-insights/spend-evidence.ts b/dev/seed/cost-insights/spend-evidence.ts new file mode 100644 index 0000000000..d3e7d6e85b --- /dev/null +++ b/dev/seed/cost-insights/spend-evidence.ts @@ -0,0 +1,734 @@ +import { randomUUID } from 'node:crypto'; + +import { computeDatabaseUrl } from '@kilocode/db'; +import { + captureCostInsightSpend, + COST_INSIGHT_DRIVER_FALLBACK, + COST_INSIGHT_KILOCLAW_PRODUCT_KEY, + type CostInsightSpendOwner, +} from '@kilocode/db/cost-insights-rollups'; +import { + api_kind, + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, + cost_insight_rollup_coverage, + credit_transactions, + feature, + kilocode_users, + microdollar_usage, + microdollar_usage_daily, + microdollar_usage_metadata, + organization_memberships, + organizations, +} from '@kilocode/db/schema'; +import type { GatewayApiKind } from '@kilocode/db/schema-types'; +import { eq, inArray, like, or, sql } from 'drizzle-orm'; + +import { getSeedDb } from '../lib/db'; +import type { SeedResult } from '../index'; + +const HOUR_MS = 60 * 60 * 1_000; +const DAY_MS = 24 * HOUR_MS; +const COVERAGE_DAYS = 90; +const BALANCE_BUFFER_MICRODOLLARS = 100_000_000; +const CREDIT_CATEGORY_PREFIX = 'dev-seed:cost-insights'; + +const PERSONAL_OWNER_ID = '4f2fc143-4b30-4c8a-878b-df89c89c6701'; +const BILLING_MANAGER_ID = '4f2fc143-4b30-4c8a-878b-df89c89c6702'; +const ORGANIZATION_MEMBER_ID = '4f2fc143-4b30-4c8a-878b-df89c89c6703'; +const ORGANIZATION_ID = '4f2fc143-4b30-4c8a-878b-df89c89c6790'; + +const PERSONAL_OWNER_EMAIL = 'cost-insights-owner@example.com'; +const BILLING_MANAGER_EMAIL = 'cost-insights-billing-manager@example.com'; +const ORGANIZATION_MEMBER_EMAIL = 'cost-insights-member@example.com'; +const ORGANIZATION_NAME = '[seed:cost-insights] Northstar Labs'; + +const PERSONAL_OWNER: CostInsightSpendOwner = { type: 'user', id: PERSONAL_OWNER_ID }; +const ORGANIZATION_OWNER: CostInsightSpendOwner = { + type: 'organization', + id: ORGANIZATION_ID, +}; +const SEED_USER_IDS = [PERSONAL_OWNER_ID, BILLING_MANAGER_ID, ORGANIZATION_MEMBER_ID]; + +export const usage = ''; + +type VariableDriver = { + featureKey: string; + apiKind: GatewayApiKind; + modelKey: string; + providerKey: string; +}; + +type VariableSpendEvent = VariableDriver & { + owner: CostInsightSpendOwner; + actorUserId: string; + occurredAt: string; + amountMicrodollars: number; +}; + +type ScheduledSpendEvent = { + owner: CostInsightSpendOwner; + actorUserId: string; + occurredAt: string; + amountMicrodollars: number; + featureKey: 'enrollment' | 'renewal'; + planKey: 'standard' | 'commit'; +}; + +const PERSONAL_DRIVERS: VariableDriver[] = [ + { + featureKey: 'cli', + apiKind: 'messages', + modelKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }, + { + featureKey: 'vscode-extension', + apiKind: 'chat_completions', + modelKey: 'openai/gpt-4.1-mini', + providerKey: 'openai', + }, + { + featureKey: 'cloud-agent', + apiKind: 'responses', + modelKey: 'google/gemini-2.5-pro', + providerKey: 'google', + }, +]; + +const ORGANIZATION_DRIVERS: VariableDriver[] = [ + { + featureKey: 'code-review', + apiKind: 'messages', + modelKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }, + { + featureKey: 'cloud-agent', + apiKind: 'responses', + modelKey: 'openai/gpt-4.1', + providerKey: 'openai', + }, + { + featureKey: 'security-agent', + apiKind: 'messages', + modelKey: 'google/gemini-2.5-pro', + providerKey: 'google', + }, +]; + +function printUsage(): void { + console.log('Usage: pnpm dev:seed cost-insights:spend-evidence'); + console.log(''); + console.log('Creates dedicated personal and organization Spend owners with 90 days of'); + console.log('canonical spend evidence and matching Cost Insights hourly rollups.'); + console.log(''); + console.log('The fixture includes current-hour anomaly spikes, rolling 24-hour spend above'); + console.log('typical test thresholds, recurring Scheduled Credit spend, and organization'); + console.log("member driver attribution. Reruns replace only this fixture's data."); +} + +function requireNoArguments(args: string[]): void { + if (args.length > 0) { + printUsage(); + throw new Error(`Unexpected arguments: ${args.join(' ')}`); + } +} + +function assertLocalDatabaseTarget(): { hostname: string; database: string; port: string } { + if (process.env.USE_PRODUCTION_DB === 'true') { + throw new Error('Cost Insights dev seed refuses to run with USE_PRODUCTION_DB=true.'); + } + + const databaseUrl = new URL(computeDatabaseUrl()); + const localHostnames = new Set(['localhost', '127.0.0.1', '::1', '[::1]']); + if (!localHostnames.has(databaseUrl.hostname)) { + throw new Error( + `Cost Insights dev seed requires a loopback database host; received ${databaseUrl.hostname}.` + ); + } + + return { + hostname: databaseUrl.hostname, + database: decodeURIComponent(databaseUrl.pathname.slice(1)), + port: databaseUrl.port || '5432', + }; +} + +function floorUtcHour(timestamp: number): number { + return Math.floor(timestamp / HOUR_MS) * HOUR_MS; +} + +function timestampAtHourOffset(currentHour: number, hourOffset: number): string { + return new Date(currentHour - hourOffset * HOUR_MS).toISOString(); +} + +function chooseByIndex(values: T[], index: number, label: string): T { + const value = values[index % values.length]; + if (value === undefined) { + throw new Error(`Missing ${label} seed value.`); + } + return value; +} + +function buildVariableSpendEvents(currentHour: number): VariableSpendEvent[] { + const events: VariableSpendEvent[] = []; + const organizationActors = [PERSONAL_OWNER_ID, BILLING_MANAGER_ID, ORGANIZATION_MEMBER_ID]; + + for (let hourOffset = 1; hourOffset <= 23; hourOffset += 1) { + const personalDriver = chooseByIndex(PERSONAL_DRIVERS, hourOffset, 'personal driver'); + events.push({ + ...personalDriver, + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: timestampAtHourOffset(currentHour, hourOffset), + amountMicrodollars: 180_000 + ((hourOffset * 47_000) % 420_000), + }); + + const organizationDriver = chooseByIndex( + ORGANIZATION_DRIVERS, + hourOffset, + 'organization driver' + ); + events.push({ + ...organizationDriver, + owner: ORGANIZATION_OWNER, + actorUserId: chooseByIndex(organizationActors, hourOffset, 'organization actor'), + occurredAt: timestampAtHourOffset(currentHour, hourOffset), + amountMicrodollars: 320_000 + ((hourOffset * 83_000) % 880_000), + }); + } + + let historicalIndex = 0; + for ( + let hourOffset = 24; + hourOffset < COVERAGE_DAYS * 24; + hourOffset += 12, historicalIndex += 1 + ) { + if (historicalIndex % 11 === 0) { + continue; + } + + const personalDriver = chooseByIndex(PERSONAL_DRIVERS, historicalIndex, 'personal driver'); + events.push({ + ...personalDriver, + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: timestampAtHourOffset(currentHour, hourOffset), + amountMicrodollars: 140_000 + ((historicalIndex * 71_000) % 760_000), + }); + + const organizationDriver = chooseByIndex( + ORGANIZATION_DRIVERS, + historicalIndex, + 'organization driver' + ); + events.push({ + ...organizationDriver, + owner: ORGANIZATION_OWNER, + actorUserId: chooseByIndex(organizationActors, historicalIndex, 'organization actor'), + occurredAt: timestampAtHourOffset(currentHour, hourOffset), + amountMicrodollars: 280_000 + ((historicalIndex * 137_000) % 1_520_000), + }); + } + + const personalSpikeAmounts = [12_000_000, 11_000_000, 9_000_000]; + for (const [index, amountMicrodollars] of personalSpikeAmounts.entries()) { + events.push({ + ...chooseByIndex(PERSONAL_DRIVERS, index, 'personal spike driver'), + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: new Date(currentHour).toISOString(), + amountMicrodollars, + }); + } + + const organizationSpikeAmounts = [18_000_000, 15_000_000, 13_000_000]; + for (const [index, amountMicrodollars] of organizationSpikeAmounts.entries()) { + events.push({ + ...chooseByIndex(ORGANIZATION_DRIVERS, index, 'organization spike driver'), + owner: ORGANIZATION_OWNER, + actorUserId: chooseByIndex(organizationActors, index, 'organization spike actor'), + occurredAt: new Date(currentHour).toISOString(), + amountMicrodollars, + }); + } + + return events; +} + +function buildScheduledSpendEvents(currentHour: number): ScheduledSpendEvent[] { + return [ + { + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: new Date(currentHour).toISOString(), + amountMicrodollars: 29_000_000, + featureKey: 'renewal', + planKey: 'standard', + }, + { + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: new Date(currentHour - 30 * DAY_MS).toISOString(), + amountMicrodollars: 29_000_000, + featureKey: 'renewal', + planKey: 'standard', + }, + { + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: new Date(currentHour - 60 * DAY_MS).toISOString(), + amountMicrodollars: 99_000_000, + featureKey: 'enrollment', + planKey: 'commit', + }, + { + owner: ORGANIZATION_OWNER, + actorUserId: BILLING_MANAGER_ID, + occurredAt: new Date(currentHour).toISOString(), + amountMicrodollars: 49_000_000, + featureKey: 'renewal', + planKey: 'standard', + }, + { + owner: ORGANIZATION_OWNER, + actorUserId: ORGANIZATION_MEMBER_ID, + occurredAt: new Date(currentHour - 30 * DAY_MS).toISOString(), + amountMicrodollars: 49_000_000, + featureKey: 'renewal', + planKey: 'standard', + }, + { + owner: ORGANIZATION_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: new Date(currentHour - 60 * DAY_MS).toISOString(), + amountMicrodollars: 149_000_000, + featureKey: 'enrollment', + planKey: 'commit', + }, + ]; +} + +function ownerColumns(owner: CostInsightSpendOwner): { + organizationId: string | null; + userId: string | null; +} { + return owner.type === 'organization' + ? { organizationId: owner.id, userId: null } + : { organizationId: null, userId: owner.id }; +} + +function sumAmounts(events: T[]): number { + return events.reduce((total, event) => total + event.amountMicrodollars, 0); +} + +function sumOwnerAmounts( + events: T[], + owner: CostInsightSpendOwner +): number { + return sumAmounts( + events.filter(event => event.owner.type === owner.type && event.owner.id === owner.id) + ); +} + +function requireLookupId( + lookup: ReadonlyMap, + value: string, + lookupName: string +): number { + const id = lookup.get(value); + if (id === undefined) { + throw new Error(`Missing ${lookupName} lookup row for ${value}.`); + } + return id; +} + +function kiloclawCreditCategory(event: ScheduledSpendEvent, index: number): string { + const sourcePrefix = + event.planKey === 'commit' ? 'kiloclaw-subscription-commit' : 'kiloclaw-subscription'; + return `${sourcePrefix}:${CREDIT_CATEGORY_PREFIX}:${index}`; +} + +function kiloclawDescription(event: ScheduledSpendEvent): string { + return `KiloClaw ${event.planKey} ${event.featureKey}`; +} + +function loginPath(email: string, callbackPath: string): string { + const params = new URLSearchParams({ fakeUser: email, callbackPath }); + return `/users/sign_in?${params.toString()}`; +} + +export async function run(...args: string[]): Promise { + if (args.includes('--help') || args.includes('-h')) { + printUsage(); + return; + } + requireNoArguments(args); + + const databaseTarget = assertLocalDatabaseTarget(); + const db = getSeedDb(); + const currentHour = floorUtcHour(Date.now()); + const currentHourIso = new Date(currentHour).toISOString(); + const coverageStartIso = new Date(currentHour - COVERAGE_DAYS * DAY_MS).toISOString(); + const variableEvents = buildVariableSpendEvents(currentHour); + const scheduledEvents = buildScheduledSpendEvents(currentHour); + + const personalVariableMicrodollars = sumOwnerAmounts(variableEvents, PERSONAL_OWNER); + const personalScheduledMicrodollars = sumOwnerAmounts(scheduledEvents, PERSONAL_OWNER); + const organizationVariableMicrodollars = sumOwnerAmounts(variableEvents, ORGANIZATION_OWNER); + const organizationScheduledMicrodollars = sumOwnerAmounts(scheduledEvents, ORGANIZATION_OWNER); + + const featureKeys = [...new Set(variableEvents.map(event => event.featureKey))]; + const apiKinds = [...new Set(variableEvents.map(event => event.apiKind))]; + + await db.transaction(async tx => { + const seedUsageIds = tx + .select({ id: microdollar_usage.id }) + .from(microdollar_usage) + .where( + or( + inArray(microdollar_usage.kilo_user_id, SEED_USER_IDS), + eq(microdollar_usage.organization_id, ORGANIZATION_ID) + ) + ); + + await tx + .delete(microdollar_usage_metadata) + .where(inArray(microdollar_usage_metadata.id, seedUsageIds)); + await tx + .delete(microdollar_usage_daily) + .where( + or( + inArray(microdollar_usage_daily.kilo_user_id, SEED_USER_IDS), + eq(microdollar_usage_daily.organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(microdollar_usage) + .where( + or( + inArray(microdollar_usage.kilo_user_id, SEED_USER_IDS), + eq(microdollar_usage.organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(credit_transactions) + .where( + or( + like( + credit_transactions.credit_category, + `kiloclaw-subscription:${CREDIT_CATEGORY_PREFIX}:%` + ), + like( + credit_transactions.credit_category, + `kiloclaw-subscription-commit:${CREDIT_CATEGORY_PREFIX}:%` + ) + ) + ); + await tx + .delete(cost_insight_owner_hour_driver_buckets) + .where( + or( + eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_hour_driver_buckets.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(cost_insight_owner_hour_totals) + .where( + or( + eq(cost_insight_owner_hour_totals.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_hour_totals.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + + const seedUsers = [ + { + id: PERSONAL_OWNER_ID, + email: PERSONAL_OWNER_EMAIL, + name: 'Morgan Lee', + stripeCustomerId: 'cus_dev_seed_cost_insights_owner', + }, + { + id: BILLING_MANAGER_ID, + email: BILLING_MANAGER_EMAIL, + name: 'Priya Shah', + stripeCustomerId: 'cus_dev_seed_cost_insights_billing', + }, + { + id: ORGANIZATION_MEMBER_ID, + email: ORGANIZATION_MEMBER_EMAIL, + name: 'Diego Santos', + stripeCustomerId: 'cus_dev_seed_cost_insights_member', + }, + ]; + + for (const user of seedUsers) { + await tx + .insert(kilocode_users) + .values({ + id: user.id, + google_user_email: user.email, + google_user_name: user.name, + google_user_image_url: `https://example.com/dev-seed/${user.id}.png`, + stripe_customer_id: user.stripeCustomerId, + normalized_email: user.email, + has_validation_stytch: true, + customer_source: 'dev-seed', + microdollars_used: 0, + total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, + }) + .onConflictDoUpdate({ + target: kilocode_users.id, + set: { + google_user_email: user.email, + google_user_name: user.name, + google_user_image_url: `https://example.com/dev-seed/${user.id}.png`, + normalized_email: user.email, + has_validation_stytch: true, + customer_source: 'dev-seed', + microdollars_used: 0, + total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, + }, + }); + } + + await tx + .insert(organizations) + .values({ + id: ORGANIZATION_ID, + name: ORGANIZATION_NAME, + created_by_kilo_user_id: PERSONAL_OWNER_ID, + plan: 'teams', + seat_count: 3, + require_seats: true, + microdollars_used: 0, + microdollars_balance: BALANCE_BUFFER_MICRODOLLARS, + total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, + }) + .onConflictDoUpdate({ + target: organizations.id, + set: { + name: ORGANIZATION_NAME, + created_by_kilo_user_id: PERSONAL_OWNER_ID, + plan: 'teams', + seat_count: 3, + require_seats: true, + deleted_at: null, + microdollars_used: 0, + microdollars_balance: BALANCE_BUFFER_MICRODOLLARS, + total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, + }, + }); + + const memberships = [ + { + organization_id: ORGANIZATION_ID, + kilo_user_id: PERSONAL_OWNER_ID, + role: 'owner', + }, + { + organization_id: ORGANIZATION_ID, + kilo_user_id: BILLING_MANAGER_ID, + role: 'billing_manager', + }, + { + organization_id: ORGANIZATION_ID, + kilo_user_id: ORGANIZATION_MEMBER_ID, + role: 'member', + }, + ] satisfies (typeof organization_memberships.$inferInsert)[]; + + for (const membership of memberships) { + await tx + .insert(organization_memberships) + .values(membership) + .onConflictDoUpdate({ + target: [organization_memberships.organization_id, organization_memberships.kilo_user_id], + set: { role: membership.role }, + }); + } + + await tx + .insert(feature) + .values(featureKeys.map(featureKey => ({ feature: featureKey }))) + .onConflictDoNothing(); + await tx + .insert(api_kind) + .values(apiKinds.map(apiKind => ({ api_kind: apiKind }))) + .onConflictDoNothing(); + + const featureRows = await tx + .select({ id: feature.feature_id, value: feature.feature }) + .from(feature) + .where(inArray(feature.feature, featureKeys)); + const apiKindRows = await tx + .select({ id: api_kind.api_kind_id, value: api_kind.api_kind }) + .from(api_kind) + .where(inArray(api_kind.api_kind, apiKinds)); + const featureIds = new Map(featureRows.map(row => [row.value, row.id])); + const apiKindIds = new Map(apiKindRows.map(row => [row.value, row.id])); + + const preparedVariableEvents = variableEvents.map((event, index) => { + const id = randomUUID(); + return { + event, + usage: { + id, + kilo_user_id: event.actorUserId, + organization_id: ownerColumns(event.owner).organizationId, + cost: event.amountMicrodollars, + input_tokens: 2_000 + (index % 8) * 750, + output_tokens: 800 + (index % 5) * 450, + cache_write_tokens: index % 3 === 0 ? 400 : 0, + cache_hit_tokens: index % 2 === 0 ? 1_200 : 0, + created_at: event.occurredAt, + provider: event.providerKey, + model: event.modelKey, + requested_model: event.modelKey, + inference_provider: event.providerKey, + has_error: false, + abuse_classification: 0, + } satisfies typeof microdollar_usage.$inferInsert, + metadata: { + id, + created_at: event.occurredAt, + message_id: `${CREDIT_CATEGORY_PREFIX}:usage:${index}`, + feature_id: requireLookupId(featureIds, event.featureKey, 'feature'), + api_kind_id: requireLookupId(apiKindIds, event.apiKind, 'API kind'), + streamed: index % 2 === 0, + is_byok: false, + is_user_byok: false, + has_tools: true, + } satisfies typeof microdollar_usage_metadata.$inferInsert, + }; + }); + + await tx.insert(microdollar_usage).values(preparedVariableEvents.map(item => item.usage)); + await tx + .insert(microdollar_usage_metadata) + .values(preparedVariableEvents.map(item => item.metadata)); + + for (const event of variableEvents) { + await captureCostInsightSpend(tx, { + owner: event.owner, + actorUserId: event.actorUserId, + occurredAt: event.occurredAt, + amountMicrodollars: event.amountMicrodollars, + category: 'variable', + source: 'ai_gateway', + productKey: event.featureKey, + featureKey: event.apiKind, + modelOrPlanKey: event.modelKey, + providerKey: event.providerKey, + }); + } + + const scheduledRows = scheduledEvents.map((event, index) => ({ + id: randomUUID(), + kilo_user_id: event.actorUserId, + organization_id: ownerColumns(event.owner).organizationId, + amount_microdollars: -event.amountMicrodollars, + is_free: false, + description: kiloclawDescription(event), + credit_category: kiloclawCreditCategory(event, index), + created_at: event.occurredAt, + check_category_uniqueness: false, + })) satisfies (typeof credit_transactions.$inferInsert)[]; + + await tx.insert(credit_transactions).values(scheduledRows); + + for (const event of scheduledEvents) { + await captureCostInsightSpend(tx, { + owner: event.owner, + actorUserId: event.actorUserId, + occurredAt: event.occurredAt, + amountMicrodollars: event.amountMicrodollars, + category: 'scheduled', + source: 'kiloclaw', + productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, + featureKey: event.featureKey, + modelOrPlanKey: event.planKey, + providerKey: COST_INSIGHT_DRIVER_FALLBACK, + }); + } + + const personalSpendMicrodollars = personalVariableMicrodollars + personalScheduledMicrodollars; + const organizationSpendMicrodollars = + organizationVariableMicrodollars + organizationScheduledMicrodollars; + + await tx + .update(kilocode_users) + .set({ + microdollars_used: personalSpendMicrodollars, + total_microdollars_acquired: personalSpendMicrodollars + BALANCE_BUFFER_MICRODOLLARS, + }) + .where(eq(kilocode_users.id, PERSONAL_OWNER_ID)); + await tx + .update(organizations) + .set({ + microdollars_used: organizationSpendMicrodollars, + microdollars_balance: BALANCE_BUFFER_MICRODOLLARS, + total_microdollars_acquired: organizationSpendMicrodollars + BALANCE_BUFFER_MICRODOLLARS, + }) + .where(eq(organizations.id, ORGANIZATION_ID)); + + await tx + .insert(cost_insight_rollup_coverage) + .values({ + rollup_version: 1, + live_capture_start_hour: currentHourIso, + coverage_start_hour: coverageStartIso, + }) + .onConflictDoUpdate({ + target: cost_insight_rollup_coverage.rollup_version, + set: { + live_capture_start_hour: sql`COALESCE(${cost_insight_rollup_coverage.live_capture_start_hour}, ${currentHourIso})`, + coverage_start_hour: sql`LEAST( + COALESCE(${cost_insight_rollup_coverage.coverage_start_hour}, ${coverageStartIso}), + ${coverageStartIso}, + COALESCE(${cost_insight_rollup_coverage.live_capture_start_hour}, ${currentHourIso}) + )`, + updated_at: sql`CURRENT_TIMESTAMP`, + }, + }); + }); + + console.log(''); + console.log('This fixture represents:'); + console.log('- 90 days of personal and organization Variable Credit spend.'); + console.log('- Monthly KiloClaw Scheduled Credit spend.'); + console.log('- Current-hour anomaly spikes and rolling 24-hour threshold crossings.'); + console.log('- Three organization members contributing distinct top spend drivers.'); + console.log(''); + console.log('Seed users are DB-only Cost Insights fixtures with placeholder Stripe IDs.'); + console.log('Use development fake login; avoid Stripe-backed billing pages with these users.'); + + return { + databaseTarget: `${databaseTarget.hostname}:${databaseTarget.port}/${databaseTarget.database}`, + personalOwnerId: PERSONAL_OWNER_ID, + personalOwnerEmail: PERSONAL_OWNER_EMAIL, + personalPath: '/cost-insights', + personalLoginPath: loginPath(PERSONAL_OWNER_EMAIL, '/cost-insights'), + organizationId: ORGANIZATION_ID, + organizationName: ORGANIZATION_NAME, + organizationPath: `/organizations/${ORGANIZATION_ID}/cost-insights`, + organizationLoginPath: loginPath( + PERSONAL_OWNER_EMAIL, + `/organizations/${ORGANIZATION_ID}/cost-insights` + ), + billingManagerId: BILLING_MANAGER_ID, + billingManagerEmail: BILLING_MANAGER_EMAIL, + organizationMemberId: ORGANIZATION_MEMBER_ID, + organizationMemberEmail: ORGANIZATION_MEMBER_EMAIL, + coverageStartHour: coverageStartIso, + currentHour: currentHourIso, + variableRecordCount: variableEvents.length, + scheduledRecordCount: scheduledEvents.length, + personalVariableMicrodollars, + personalScheduledMicrodollars, + organizationVariableMicrodollars, + organizationScheduledMicrodollars, + }; +} diff --git a/packages/db/package.json b/packages/db/package.json index 2f63e10db7..cd182df7a9 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -11,6 +11,7 @@ ".": "./src/index.ts", "./schema": "./src/schema.ts", "./schema-types": "./src/schema-types.ts", + "./cost-insights-rollups": "./src/cost-insights-rollups.ts", "./kiloclaw-pricing-catalog": "./src/kiloclaw-pricing-catalog.ts", "./kiloclaw-commit-retirement": "./src/kiloclaw-commit-retirement.ts", "./kiloclaw-organization-trial-expiry-candidates": "./src/kiloclaw-organization-trial-expiry-candidates.ts", diff --git a/packages/db/src/cost-insights-rollups.test.ts b/packages/db/src/cost-insights-rollups.test.ts new file mode 100644 index 0000000000..985ded7788 --- /dev/null +++ b/packages/db/src/cost-insights-rollups.test.ts @@ -0,0 +1,585 @@ +import { afterAll, describe, expect, it } from '@jest/globals'; +import { eq, or, sql } from 'drizzle-orm'; + +import { createDrizzleClient } from './client'; +import { + COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH, + buildCostInsightDriver, + captureCostInsightSpend, + getCostInsightUtcHourStart, + normalizeCostInsightDriverDimension, + type CaptureCostInsightSpendInput, + type CostInsightRollupTransactionWriter, +} from './cost-insights-rollups'; +import { computeDatabaseUrl } from './database-url'; +import { + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, + cost_insight_rollup_coverage, + cost_insight_rollup_degraded_intervals, + kilocode_users, + organizations, +} from './schema'; +import { + CostInsightRollupDegradedReason, + CostInsightSpendCategory, + CostInsightSpendSource, +} from './schema-types'; + +const testDatabase = createDrizzleClient({ + connectionString: computeDatabaseUrl(), + poolConfig: { application_name: 'cost-insights-rollups-test', max: 4 }, +}); + +type CostInsightTestFixture = { + userId: string; + organizationId: string; +}; + +async function withCostInsightFixture( + testFn: (fixture: CostInsightTestFixture) => Promise +): Promise { + const uniqueId = crypto.randomUUID(); + const userId = `cost-insights-${uniqueId}`; + const organizationId = crypto.randomUUID(); + + await testDatabase.db.insert(kilocode_users).values({ + id: userId, + google_user_email: `${userId}@example.com`, + google_user_name: 'Cost Insights Test User', + google_user_image_url: 'https://example.com/avatar.png', + stripe_customer_id: `cus_${uniqueId}`, + }); + await testDatabase.db.insert(organizations).values({ + id: organizationId, + name: `Cost Insights ${uniqueId}`, + }); + + try { + await testFn({ userId, organizationId }); + } finally { + await testDatabase.db + .delete(cost_insight_owner_hour_driver_buckets) + .where( + or( + eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId), + eq(cost_insight_owner_hour_driver_buckets.owned_by_organization_id, organizationId), + eq(cost_insight_owner_hour_driver_buckets.actor_user_id, userId) + ) + ); + await testDatabase.db + .delete(cost_insight_owner_hour_totals) + .where( + or( + eq(cost_insight_owner_hour_totals.owned_by_user_id, userId), + eq(cost_insight_owner_hour_totals.owned_by_organization_id, organizationId) + ) + ); + await testDatabase.db.delete(organizations).where(eq(organizations.id, organizationId)); + await testDatabase.db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); + } +} + +function captureInput( + fixture: CostInsightTestFixture, + overrides: Partial = {} +): CaptureCostInsightSpendInput { + return { + owner: { type: 'user', id: fixture.userId }, + actorUserId: fixture.userId, + occurredAt: '2026-11-01T01:30:00-04:00', + amountMicrodollars: 125, + category: CostInsightSpendCategory.Variable, + source: CostInsightSpendSource.AiGateway, + productKey: 'direct-gateway', + featureKey: 'chat_completions', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + ...overrides, + }; +} + +async function expectConstraintViolation( + insertPromise: Promise, + constraint: string +): Promise { + await expect(insertPromise).rejects.toMatchObject({ + cause: { constraint }, + }); +} + +afterAll(async () => { + await testDatabase.pool.end(); +}); + +describe('Cost Insights rollup capture', () => { + it('uses explicit UTC hour buckets across DST offset changes', () => { + expect(getCostInsightUtcHourStart('2026-11-01T01:30:00-04:00')).toBe( + '2026-11-01T05:00:00.000Z' + ); + expect(getCostInsightUtcHourStart('2026-11-01T01:30:00-05:00')).toBe( + '2026-11-01T06:00:00.000Z' + ); + expect(getCostInsightUtcHourStart('2026-04-29 01:16:12.945+00')).toBe( + '2026-04-29T01:00:00.000Z' + ); + }); + + it('normalizes bounded identifiers and keeps the v1 driver digest stable', async () => { + expect(normalizeCostInsightDriverDimension('')).toBe('other'); + expect(normalizeCostInsightDriverDimension('request label with spaces')).toBe('other'); + expect( + normalizeCostInsightDriverDimension('x'.repeat(COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH + 5)) + ).toHaveLength(COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH); + + await expect( + buildCostInsightDriver({ + source: CostInsightSpendSource.AiGateway, + productKey: ' direct-gateway ', + featureKey: 'chat_completions', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + actorUserId: 'user-123', + }) + ).resolves.toEqual({ + source: CostInsightSpendSource.AiGateway, + productKey: 'direct-gateway', + featureKey: 'chat_completions', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + actorUserId: 'user-123', + driverKey: '03dcdfd758bed98d48c43ec1ebf68e101ba2de835422bce541ccb1e2e56a3783', + }); + }); + + it.each([ + { + description: 'missing owner ID', + overrides: { owner: { type: 'user' as const, id: '' } }, + error: 'cost_insight_invalid_owner_id', + }, + { + description: 'timestamp without an explicit timezone', + overrides: { occurredAt: '2026-06-25T12:30:00' }, + error: 'cost_insight_invalid_occurred_at', + }, + { + description: 'invalid calendar timestamp', + overrides: { occurredAt: '2026-02-30T12:30:00Z' }, + error: 'cost_insight_invalid_occurred_at', + }, + { + description: 'unsafe amount', + overrides: { amountMicrodollars: Number.MAX_SAFE_INTEGER + 1 }, + error: 'cost_insight_invalid_amount_microdollars', + }, + { + description: 'non-positive count', + overrides: { spendRecordCount: 0 }, + error: 'cost_insight_invalid_spend_record_count', + }, + { + description: 'uncontrolled category', + overrides: { category: 'refund' as CostInsightSpendCategory }, + error: 'cost_insight_invalid_category', + }, + { + description: 'uncontrolled source', + overrides: { source: 'exa' as CostInsightSpendSource }, + error: 'cost_insight_invalid_source', + }, + ])('rejects $description before writing', async ({ overrides, error }) => { + await withCostInsightFixture(async fixture => { + await expect( + testDatabase.db.transaction(tx => + captureCostInsightSpend(tx, captureInput(fixture, overrides)) + ) + ).rejects.toThrow(error); + }); + }); + + it('writes lock, total, and driver through one database round trip', async () => { + const execute = jest.fn(async () => ({ rows: [{ outcome: 'ok' }] })); + const transaction = { execute } as unknown as CostInsightRollupTransactionWriter; + + await captureCostInsightSpend( + transaction, + captureInput({ userId: 'user-1', organizationId: crypto.randomUUID() }) + ); + + expect(execute).toHaveBeenCalledTimes(1); + }); + + it('adds totals before matching driver buckets under a non-UTC database timezone', async () => { + await withCostInsightFixture(async fixture => { + await testDatabase.db.transaction(async tx => { + await tx.execute(sql`SET LOCAL TIME ZONE 'America/Los_Angeles'`); + await captureCostInsightSpend(tx, captureInput(fixture)); + await captureCostInsightSpend( + tx, + captureInput(fixture, { + occurredAt: '2026-11-01T05:55:00.000Z', + amountMicrodollars: 75, + spendRecordCount: 3, + }) + ); + }); + + const totals = await testDatabase.db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); + const drivers = await testDatabase.db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, fixture.userId)); + + expect(totals).toHaveLength(1); + expect(totals[0]).toMatchObject({ + spend_category: CostInsightSpendCategory.Variable, + total_microdollars: 200, + spend_record_count: 4, + }); + expect(new Date(totals[0]?.hour_start ?? '').toISOString()).toBe('2026-11-01T05:00:00.000Z'); + expect(drivers).toHaveLength(1); + expect(drivers[0]).toMatchObject({ + total_microdollars: 200, + spend_record_count: 4, + }); + expect(new Date(drivers[0]?.hour_start ?? '').toISOString()).toBe('2026-11-01T05:00:00.000Z'); + }); + }); + + it('keeps personal and organization owner-hour rows isolated', async () => { + await withCostInsightFixture(async fixture => { + await testDatabase.db.transaction(async tx => { + await captureCostInsightSpend(tx, captureInput(fixture)); + await captureCostInsightSpend( + tx, + captureInput(fixture, { + owner: { type: 'organization', id: fixture.organizationId }, + }) + ); + }); + + const totals = await testDatabase.db + .select() + .from(cost_insight_owner_hour_totals) + .where( + or( + eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId), + eq(cost_insight_owner_hour_totals.owned_by_organization_id, fixture.organizationId) + ) + ); + const drivers = await testDatabase.db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where( + or( + eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, fixture.userId), + eq( + cost_insight_owner_hour_driver_buckets.owned_by_organization_id, + fixture.organizationId + ) + ) + ); + + expect(totals).toHaveLength(2); + expect(drivers).toHaveLength(2); + }); + }); + + it('serializes concurrent owner-hour captures and preserves exact sums', async () => { + await withCostInsightFixture(async fixture => { + await Promise.all( + Array.from({ length: 8 }, () => + testDatabase.db.transaction(tx => + captureCostInsightSpend( + tx, + captureInput(fixture, { amountMicrodollars: 7, spendRecordCount: 2 }) + ) + ) + ) + ); + + const [total] = await testDatabase.db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); + const [driver] = await testDatabase.db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, fixture.userId)); + + expect(total).toMatchObject({ total_microdollars: 56, spend_record_count: 16 }); + expect(driver).toMatchObject({ total_microdollars: 56, spend_record_count: 16 }); + }); + }); + + it('rejects a digest collision and rolls back the preceding total upsert', async () => { + await withCostInsightFixture(async fixture => { + const input = captureInput(fixture); + const driver = await buildCostInsightDriver(input); + const hourStart = getCostInsightUtcHourStart(input.occurredAt); + + await testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + owned_by_user_id: fixture.userId, + owned_by_organization_id: null, + hour_start: hourStart, + spend_category: input.category, + driver_key: driver.driverKey, + source: driver.source, + product_key: 'different-product', + feature_key: driver.featureKey, + model_or_plan_key: driver.modelOrPlanKey, + provider_key: driver.providerKey, + actor_user_id: driver.actorUserId, + total_microdollars: 1, + spend_record_count: 1, + }); + + await expect( + testDatabase.db.transaction(tx => captureCostInsightSpend(tx, input)) + ).rejects.toThrow('cost_insight_driver_digest_collision'); + + const totals = await testDatabase.db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); + const [storedDriver] = await testDatabase.db + .select() + .from(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, fixture.userId)); + + expect(totals).toHaveLength(0); + expect(storedDriver).toMatchObject({ + product_key: 'different-product', + total_microdollars: 1, + spend_record_count: 1, + }); + }); + }); +}); + +describe('Cost Insights rollup constraints', () => { + it('enforces total owner, hour, category, positive, and safe-integer contracts', async () => { + await withCostInsightFixture(async fixture => { + const valid = { + owned_by_user_id: fixture.userId, + owned_by_organization_id: null, + hour_start: '2026-06-25T12:00:00.000Z', + spend_category: CostInsightSpendCategory.Variable, + total_microdollars: 1, + spend_record_count: 1, + }; + + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_totals).values({ + ...valid, + owned_by_user_id: null, + }), + 'cost_insight_owner_hour_totals_owner_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_totals).values({ + ...valid, + hour_start: '2026-06-25T12:00:01.000Z', + }), + 'cost_insight_owner_hour_totals_hour_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_totals).values({ + ...valid, + spend_category: 'refund' as CostInsightSpendCategory, + }), + 'cost_insight_owner_hour_totals_category_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_totals).values({ + ...valid, + total_microdollars: 0, + }), + 'cost_insight_owner_hour_totals_amount_positive_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_totals).values({ + ...valid, + total_microdollars: Number.MAX_SAFE_INTEGER + 1, + }), + 'cost_insight_owner_hour_totals_amount_safe_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_totals).values({ + ...valid, + spend_record_count: 0, + }), + 'cost_insight_owner_hour_totals_count_positive_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_totals).values({ + ...valid, + spend_record_count: Number.MAX_SAFE_INTEGER + 1, + }), + 'cost_insight_owner_hour_totals_count_safe_check' + ); + }); + }); + + it('enforces driver digest, dimension, source, and owner contracts', async () => { + await withCostInsightFixture(async fixture => { + const valid = { + owned_by_user_id: fixture.userId, + owned_by_organization_id: null, + hour_start: '2026-06-25T12:00:00.000Z', + spend_category: CostInsightSpendCategory.Variable, + driver_key: 'a'.repeat(64), + source: CostInsightSpendSource.AiGateway, + product_key: 'direct-gateway', + feature_key: 'chat_completions', + model_or_plan_key: 'anthropic/claude-sonnet-4', + provider_key: 'anthropic', + actor_user_id: fixture.userId, + total_microdollars: 1, + spend_record_count: 1, + }; + + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + ...valid, + owned_by_user_id: null, + }), + 'cost_insight_driver_buckets_owner_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + ...valid, + driver_key: 'not-a-digest', + }), + 'cost_insight_driver_buckets_driver_key_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + ...valid, + source: 'exa' as CostInsightSpendSource, + }), + 'cost_insight_driver_buckets_source_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + ...valid, + product_key: 'x'.repeat(COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH + 1), + }), + 'cost_insight_driver_buckets_product_key_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + ...valid, + total_microdollars: 0, + }), + 'cost_insight_driver_buckets_amount_positive_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + ...valid, + total_microdollars: Number.MAX_SAFE_INTEGER + 1, + }), + 'cost_insight_driver_buckets_amount_safe_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + ...valid, + spend_record_count: 0, + }), + 'cost_insight_driver_buckets_count_positive_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ + ...valid, + spend_record_count: Number.MAX_SAFE_INTEGER + 1, + }), + 'cost_insight_driver_buckets_count_safe_check' + ); + }); + }); + + it('enforces coverage and degraded interval hour/range taxonomies', async () => { + const version = 31_999; + const intervalIds: string[] = []; + + try { + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_rollup_coverage).values({ + rollup_version: version, + live_capture_start_hour: '2026-06-25T12:30:00.000Z', + }), + 'cost_insight_rollup_coverage_live_hour_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_rollup_coverage).values({ + rollup_version: version, + coverage_start_hour: '2026-06-25T12:00:00.000Z', + }), + 'cost_insight_rollup_coverage_range_check' + ); + + const degradedBase = { + start_hour: '2026-06-25T12:00:00.000Z', + end_hour_exclusive: '2026-06-25T13:00:00.000Z', + source: CostInsightSpendSource.AiGateway, + reason: CostInsightRollupDegradedReason.ReconciliationMismatch, + }; + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ + ...degradedBase, + start_hour: '2026-06-25T12:00:01.000Z', + }), + 'cost_insight_degraded_intervals_start_hour_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ + ...degradedBase, + end_hour_exclusive: degradedBase.start_hour, + }), + 'cost_insight_degraded_intervals_range_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ + ...degradedBase, + detected_at: '2026-06-25T12:30:00.000Z', + resolved_at: '2026-06-25T12:29:59.999Z', + }), + 'cost_insight_degraded_intervals_resolution_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ + ...degradedBase, + reason: 'freeform-error' as CostInsightRollupDegradedReason, + }), + 'cost_insight_degraded_intervals_reason_check' + ); + await expectConstraintViolation( + testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ + ...degradedBase, + source: 'exa' as CostInsightSpendSource, + }), + 'cost_insight_degraded_intervals_source_check' + ); + + const [interval] = await testDatabase.db + .insert(cost_insight_rollup_degraded_intervals) + .values(degradedBase) + .returning({ id: cost_insight_rollup_degraded_intervals.id }); + if (!interval) throw new Error('failed_to_insert_cost_insight_degraded_interval'); + intervalIds.push(interval.id); + } finally { + for (const id of intervalIds) { + await testDatabase.db + .delete(cost_insight_rollup_degraded_intervals) + .where(eq(cost_insight_rollup_degraded_intervals.id, id)); + } + await testDatabase.db + .delete(cost_insight_rollup_coverage) + .where(eq(cost_insight_rollup_coverage.rollup_version, version)); + } + }); +}); diff --git a/packages/db/src/cost-insights-rollups.ts b/packages/db/src/cost-insights-rollups.ts new file mode 100644 index 0000000000..21d52a0f27 --- /dev/null +++ b/packages/db/src/cost-insights-rollups.ts @@ -0,0 +1,435 @@ +import { createHash } from 'node:crypto'; + +import { sql, type SQL } from 'drizzle-orm'; + +import type { WorkerDb } from './client'; +import { cost_insight_owner_hour_driver_buckets, cost_insight_owner_hour_totals } from './schema'; +import { + CostInsightSpendCategory, + CostInsightSpendSource, + type CostInsightSpendCategory as CostInsightSpendCategoryType, + type CostInsightSpendSource as CostInsightSpendSourceType, +} from './schema-types'; + +export const COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH = 128; +export const COST_INSIGHT_DRIVER_FALLBACK = 'other'; +export const COST_INSIGHT_CODING_PLAN_PRODUCT_KEY = 'coding-plan'; +export const COST_INSIGHT_EXA_PRODUCT_KEY = 'exa'; +export const COST_INSIGHT_KILOCLAW_PRODUCT_KEY = 'kiloclaw-hosting'; + +const DRIVER_KEY_SERIALIZATION_VERSION = 'cost-insight-driver-key:v1'; +const DRIVER_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/; +const TIMESTAMP_WITH_TIMEZONE_PATTERN = + /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}(?::?\d{2})?)$/i; +const spendCategories = new Set(Object.values(CostInsightSpendCategory)); +const spendSources = new Set(Object.values(CostInsightSpendSource)); + +export type CostInsightSpendOwner = + | { type: 'user'; id: string } + | { type: 'organization'; id: string }; + +export type CostInsightDriverInput = { + source: CostInsightSpendSourceType; + productKey: string; + featureKey: string; + modelOrPlanKey: string; + providerKey: string; + actorUserId: string; +}; + +export type CostInsightDriver = { + source: CostInsightSpendSourceType; + productKey: string; + featureKey: string; + modelOrPlanKey: string; + providerKey: string; + actorUserId: string; + driverKey: string; +}; + +export type CaptureCostInsightSpendInput = CostInsightDriverInput & { + owner: CostInsightSpendOwner; + occurredAt: string; + amountMicrodollars: number; + spendRecordCount?: number; + category: CostInsightSpendCategoryType; +}; + +export type CostInsightRollupTransactionWriter = Pick; + +function assertIdentifier(value: string, errorCode: string): void { + if (value.length === 0 || value.trim() !== value) { + throw new Error(errorCode); + } +} + +function assertOwner(owner: unknown): asserts owner is CostInsightSpendOwner { + if ( + typeof owner !== 'object' || + owner === null || + !('type' in owner) || + (owner.type !== 'user' && owner.type !== 'organization') + ) { + throw new Error('cost_insight_invalid_owner_type'); + } + if (!('id' in owner) || typeof owner.id !== 'string') { + throw new Error('cost_insight_invalid_owner_id'); + } + assertIdentifier(owner.id, 'cost_insight_invalid_owner_id'); +} + +function assertPositiveSafeInteger(value: number, errorCode: string): void { + if (!Number.isSafeInteger(value) || value <= 0) { + throw new Error(errorCode); + } +} + +function hasValidTimestampFields(value: string): boolean { + const match = TIMESTAMP_WITH_TIMEZONE_PATTERN.exec(value); + if (!match) return false; + + const fields = match.slice(1, 7).map(Number); + const [year, month, day, hour, minute, second] = fields; + const timezone = match[7]; + if ( + year === undefined || + month === undefined || + day === undefined || + hour === undefined || + minute === undefined || + second === undefined || + timezone === undefined + ) { + return false; + } + + const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + const daysPerMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const daysInMonth = daysPerMonth[month - 1]; + if ( + daysInMonth === undefined || + day < 1 || + day > daysInMonth || + hour > 23 || + minute > 59 || + second > 59 + ) { + return false; + } + + if (timezone.toUpperCase() === 'Z') return true; + + const offset = timezone.slice(1).replace(':', ''); + const offsetHour = Number(offset.slice(0, 2)); + const offsetMinute = offset.length === 2 ? 0 : Number(offset.slice(2)); + return offsetHour <= 23 && offsetMinute <= 59; +} + +function parseTimestamp(value: string): Date { + if ( + typeof value !== 'string' || + value.length === 0 || + value.trim() !== value || + !hasValidTimestampFields(value) + ) { + throw new Error('cost_insight_invalid_occurred_at'); + } + + const parsed = new Date(value); + if (!Number.isFinite(parsed.getTime())) { + throw new Error('cost_insight_invalid_occurred_at'); + } + return parsed; +} + +export function getCostInsightUtcHourStart(occurredAt: string): string { + const parsed = parseTimestamp(occurredAt); + parsed.setUTCMinutes(0, 0, 0); + return parsed.toISOString(); +} + +export function normalizeCostInsightDriverDimension(value: unknown): string { + if (typeof value !== 'string') return COST_INSIGHT_DRIVER_FALLBACK; + + const normalized = value.trim(); + if (normalized.length === 0 || !DRIVER_IDENTIFIER_PATTERN.test(normalized)) { + return COST_INSIGHT_DRIVER_FALLBACK; + } + + return normalized.slice(0, COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH); +} + +function serializeLengthPrefixedUtf8(values: readonly string[]): Uint8Array { + const encoder = new TextEncoder(); + const encodedValues = values.map(value => encoder.encode(value)); + const totalLength = encodedValues.reduce((length, value) => length + 4 + value.byteLength, 0); + const serialized = new Uint8Array(totalLength); + let offset = 0; + + for (const value of encodedValues) { + const length = value.byteLength; + serialized[offset] = (length >>> 24) & 0xff; + serialized[offset + 1] = (length >>> 16) & 0xff; + serialized[offset + 2] = (length >>> 8) & 0xff; + serialized[offset + 3] = length & 0xff; + serialized.set(value, offset + 4); + offset += 4 + length; + } + + return serialized; +} + +function sha256Hex(value: Uint8Array): string { + return createHash('sha256').update(value).digest('hex'); +} + +export async function buildCostInsightDriver( + input: CostInsightDriverInput +): Promise { + if (!spendSources.has(input.source)) { + throw new Error('cost_insight_invalid_source'); + } + if (typeof input.actorUserId !== 'string') { + throw new Error('cost_insight_invalid_actor_user_id'); + } + assertIdentifier(input.actorUserId, 'cost_insight_invalid_actor_user_id'); + + const driver = { + source: input.source, + productKey: normalizeCostInsightDriverDimension(input.productKey), + featureKey: normalizeCostInsightDriverDimension(input.featureKey), + modelOrPlanKey: normalizeCostInsightDriverDimension(input.modelOrPlanKey), + providerKey: normalizeCostInsightDriverDimension(input.providerKey), + actorUserId: input.actorUserId, + }; + const serialized = serializeLengthPrefixedUtf8([ + DRIVER_KEY_SERIALIZATION_VERSION, + driver.source, + driver.productKey, + driver.featureKey, + driver.modelOrPlanKey, + driver.providerKey, + driver.actorUserId, + ]); + + return { + ...driver, + driverKey: sha256Hex(serialized), + }; +} + +function ownerColumns(owner: CostInsightSpendOwner): { + owned_by_user_id: string | null; + owned_by_organization_id: string | null; +} { + return owner.type === 'user' + ? { owned_by_user_id: owner.id, owned_by_organization_id: null } + : { owned_by_user_id: null, owned_by_organization_id: owner.id }; +} + +function buildCostInsightOwnerHourLockKey(owner: CostInsightSpendOwner, hourStart: string): string { + return [ + 'cost-insight-owner-hour:v1', + `${owner.type.length}:${owner.type}`, + `${owner.id.length}:${owner.id}`, + `${hourStart.length}:${hourStart}`, + ].join('|'); +} + +export async function acquireCostInsightOwnerHourLock( + tx: CostInsightRollupTransactionWriter, + owner: CostInsightSpendOwner, + hourStart: string +): Promise { + assertOwner(owner); + const normalizedHourStart = getCostInsightUtcHourStart(hourStart); + const lockKey = buildCostInsightOwnerHourLockKey(owner, normalizedHourStart); + + await tx.execute( + sql`SELECT pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(${lockKey}, 0::bigint))` + ); +} + +type CostInsightCaptureOutcome = { + outcome: 'ok' | 'cost_insight_driver_digest_collision'; +}; + +function costInsightConflictTargets(owner: CostInsightSpendOwner): { + total: SQL; + driver: SQL; +} { + return owner.type === 'user' + ? { + total: sql.raw(` + (owned_by_user_id, hour_start, spend_category) + WHERE owned_by_organization_id IS NULL + `), + driver: sql.raw(` + (owned_by_user_id, hour_start, spend_category, driver_key) + WHERE owned_by_organization_id IS NULL + `), + } + : { + total: sql.raw(` + (owned_by_organization_id, hour_start, spend_category) + WHERE owned_by_user_id IS NULL + `), + driver: sql.raw(` + (owned_by_organization_id, hour_start, spend_category, driver_key) + WHERE owned_by_user_id IS NULL + `), + }; +} + +async function writeCostInsightSpend( + tx: CostInsightRollupTransactionWriter, + values: { + owner: CostInsightSpendOwner; + ownedByUserId: string | null; + ownedByOrganizationId: string | null; + hourStart: string; + category: CostInsightSpendCategoryType; + driver: CostInsightDriver; + amountMicrodollars: number; + spendRecordCount: number; + } +): Promise { + const conflictTargets = costInsightConflictTargets(values.owner); + const lockKey = buildCostInsightOwnerHourLockKey(values.owner, values.hourStart); + const result = await tx.execute(sql` + WITH capture_input AS MATERIALIZED ( + SELECT + ${values.ownedByUserId}::text AS owned_by_user_id, + ${values.ownedByOrganizationId}::uuid AS owned_by_organization_id, + ${values.hourStart}::timestamptz AS hour_start, + ${values.category}::text AS spend_category, + ${values.driver.driverKey}::text AS driver_key, + ${values.driver.source}::text AS source, + ${values.driver.productKey}::text AS product_key, + ${values.driver.featureKey}::text AS feature_key, + ${values.driver.modelOrPlanKey}::text AS model_or_plan_key, + ${values.driver.providerKey}::text AS provider_key, + ${values.driver.actorUserId}::text AS actor_user_id, + ${values.amountMicrodollars}::bigint AS amount_microdollars, + ${values.spendRecordCount}::bigint AS spend_record_count, + ${lockKey}::text AS lock_key + ), owner_hour_lock AS MATERIALIZED ( + SELECT pg_catalog.pg_advisory_xact_lock( + pg_catalog.hashtextextended(capture_input.lock_key, 0::bigint) + ) AS acquired + FROM capture_input + ), owner_total_upsert AS ( + INSERT INTO ${cost_insight_owner_hour_totals} AS current_total ( + owned_by_user_id, + owned_by_organization_id, + hour_start, + spend_category, + total_microdollars, + spend_record_count + ) + SELECT + capture_input.owned_by_user_id, + capture_input.owned_by_organization_id, + capture_input.hour_start, + capture_input.spend_category, + capture_input.amount_microdollars, + capture_input.spend_record_count + FROM capture_input + CROSS JOIN owner_hour_lock + WHERE TRUE + ON CONFLICT ${conflictTargets.total} + DO UPDATE SET + total_microdollars = current_total.total_microdollars + excluded.total_microdollars, + spend_record_count = current_total.spend_record_count + excluded.spend_record_count, + updated_at = pg_catalog.now() + RETURNING 1 AS upserted + ), driver_upsert AS ( + INSERT INTO ${cost_insight_owner_hour_driver_buckets} AS current_driver ( + owned_by_user_id, + owned_by_organization_id, + hour_start, + spend_category, + driver_key, + source, + product_key, + feature_key, + model_or_plan_key, + provider_key, + actor_user_id, + total_microdollars, + spend_record_count + ) + SELECT + capture_input.owned_by_user_id, + capture_input.owned_by_organization_id, + capture_input.hour_start, + capture_input.spend_category, + capture_input.driver_key, + capture_input.source, + capture_input.product_key, + capture_input.feature_key, + capture_input.model_or_plan_key, + capture_input.provider_key, + capture_input.actor_user_id, + capture_input.amount_microdollars, + capture_input.spend_record_count + FROM capture_input + CROSS JOIN owner_total_upsert + WHERE TRUE + ON CONFLICT ${conflictTargets.driver} + DO UPDATE SET + total_microdollars = current_driver.total_microdollars + excluded.total_microdollars, + spend_record_count = current_driver.spend_record_count + excluded.spend_record_count, + updated_at = pg_catalog.now() + WHERE current_driver.source = excluded.source + AND current_driver.product_key = excluded.product_key + AND current_driver.feature_key = excluded.feature_key + AND current_driver.model_or_plan_key = excluded.model_or_plan_key + AND current_driver.provider_key = excluded.provider_key + AND current_driver.actor_user_id = excluded.actor_user_id + RETURNING 'ok'::text AS outcome + ) + SELECT COALESCE( + (SELECT outcome FROM driver_upsert), + 'cost_insight_driver_digest_collision' + ) AS outcome + `); + const outcome = result.rows[0]?.outcome; + if (outcome === 'cost_insight_driver_digest_collision') { + throw new Error('cost_insight_driver_digest_collision'); + } + if (outcome !== 'ok') { + throw new Error('cost_insight_rollup_write_missing_outcome'); + } +} + +export async function captureCostInsightSpend( + tx: CostInsightRollupTransactionWriter, + input: CaptureCostInsightSpendInput +): Promise { + assertOwner(input.owner); + if (!spendCategories.has(input.category)) { + throw new Error('cost_insight_invalid_category'); + } + if (!spendSources.has(input.source)) { + throw new Error('cost_insight_invalid_source'); + } + assertPositiveSafeInteger(input.amountMicrodollars, 'cost_insight_invalid_amount_microdollars'); + const spendRecordCount = input.spendRecordCount === undefined ? 1 : input.spendRecordCount; + assertPositiveSafeInteger(spendRecordCount, 'cost_insight_invalid_spend_record_count'); + + const hourStart = getCostInsightUtcHourStart(input.occurredAt); + const driver = await buildCostInsightDriver(input); + const owner = ownerColumns(input.owner); + + await writeCostInsightSpend(tx, { + owner: input.owner, + ownedByUserId: owner.owned_by_user_id, + ownedByOrganizationId: owner.owned_by_organization_id, + hourStart, + category: input.category, + driver, + amountMicrodollars: input.amountMicrodollars, + spendRecordCount, + }); +} diff --git a/packages/db/src/schema-types.ts b/packages/db/src/schema-types.ts index 635352e271..e72ca36545 100644 --- a/packages/db/src/schema-types.ts +++ b/packages/db/src/schema-types.ts @@ -595,6 +595,35 @@ export const CodingPlanTermKind = { export type CodingPlanTermKind = (typeof CodingPlanTermKind)[keyof typeof CodingPlanTermKind]; +// --- Cost Insights enums --- + +export const CostInsightSpendCategory = { + Variable: 'variable', + Scheduled: 'scheduled', +} as const; + +export type CostInsightSpendCategory = + (typeof CostInsightSpendCategory)[keyof typeof CostInsightSpendCategory]; + +export const CostInsightSpendSource = { + AiGateway: 'ai_gateway', + KiloClaw: 'kiloclaw', + CodingPlan: 'coding_plan', + Other: 'other', +} as const; + +export type CostInsightSpendSource = + (typeof CostInsightSpendSource)[keyof typeof CostInsightSpendSource]; + +export const CostInsightRollupDegradedReason = { + CaptureBypass: 'capture_bypass', + ReconciliationMismatch: 'reconciliation_mismatch', + LateSourceData: 'late_source_data', +} as const; + +export type CostInsightRollupDegradedReason = + (typeof CostInsightRollupDegradedReason)[keyof typeof CostInsightRollupDegradedReason]; + // NOTE: Do not change these action names. Use present tense for consistency. export const KiloClawAdminAuditAction = z.enum([ 'kiloclaw.volume.extend', diff --git a/packages/db/src/schema.test.ts b/packages/db/src/schema.test.ts index 0fc19905ed..ad144a8682 100644 --- a/packages/db/src/schema.test.ts +++ b/packages/db/src/schema.test.ts @@ -533,6 +533,13 @@ describe('database schema', () => { ], CodingPlanSubscriptionStatus: ['active', 'past_due', 'canceled'], CodingPlanTermKind: ['activation', 'extension', 'renewal'], + CostInsightSpendCategory: ['variable', 'scheduled'], + CostInsightSpendSource: ['ai_gateway', 'kiloclaw', 'coding_plan', 'other'], + CostInsightRollupDegradedReason: [ + 'capture_bypass', + 'reconciliation_mismatch', + 'late_source_data', + ], CodeReviewAnalyticsCaptureStatus: ['captured', 'missing', 'invalid', 'omitted'], CodeReviewAnalyticsChangeType: [ 'bug_fix', diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 0a2d453346..2c95993053 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -85,6 +85,9 @@ import { CodingPlanCredentialStatus, CodingPlanSubscriptionStatus, CodingPlanTermKind, + CostInsightSpendCategory, + CostInsightSpendSource, + CostInsightRollupDegradedReason, CODE_REVIEW_ANALYTICS_SCHEMA_VERSION, CODE_REVIEW_ANALYTICS_TAXONOMY_VERSION, CodeReviewAnalyticsCaptureStatus, @@ -234,6 +237,9 @@ export const SCHEMA_CHECK_ENUMS = { CodingPlanCredentialStatus, CodingPlanSubscriptionStatus, CodingPlanTermKind, + CostInsightSpendCategory, + CostInsightSpendSource, + CostInsightRollupDegradedReason, CodeReviewAnalyticsCaptureStatus, CodeReviewAnalyticsChangeType, CodeReviewAnalyticsImpactLevel, @@ -2669,6 +2675,251 @@ export const organizations = pgTable( export type Organization = typeof organizations.$inferSelect; +export const cost_insight_owner_hour_totals = pgTable( + 'cost_insight_owner_hour_totals', + { + id: uuid() + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey() + .notNull(), + owned_by_user_id: text().references(() => kilocode_users.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + owned_by_organization_id: uuid().references(() => organizations.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + hour_start: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + spend_category: text().$type().notNull(), + total_microdollars: bigint({ mode: 'number' }).notNull(), + spend_record_count: bigint({ mode: 'number' }).notNull(), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + uniqueIndex('UQ_cost_insight_owner_hour_totals_user') + .on(table.owned_by_user_id, table.hour_start, table.spend_category) + .where(isNull(table.owned_by_organization_id)), + uniqueIndex('UQ_cost_insight_owner_hour_totals_org') + .on(table.owned_by_organization_id, table.hour_start, table.spend_category) + .where(isNull(table.owned_by_user_id)), + index('IDX_cost_insight_owner_hour_totals_hour').on(table.hour_start), + check( + 'cost_insight_owner_hour_totals_owner_check', + sql`(${table.owned_by_user_id} IS NOT NULL AND ${table.owned_by_organization_id} IS NULL) OR (${table.owned_by_user_id} IS NULL AND ${table.owned_by_organization_id} IS NOT NULL)` + ), + check( + 'cost_insight_owner_hour_totals_hour_check', + sql`${table.hour_start} = date_trunc('hour', ${table.hour_start}, 'UTC')` + ), + enumCheck( + 'cost_insight_owner_hour_totals_category_check', + table.spend_category, + CostInsightSpendCategory + ), + check( + 'cost_insight_owner_hour_totals_amount_positive_check', + sql`${table.total_microdollars} > 0` + ), + check( + 'cost_insight_owner_hour_totals_amount_safe_check', + sql`${table.total_microdollars} <= 9007199254740991` + ), + check( + 'cost_insight_owner_hour_totals_count_positive_check', + sql`${table.spend_record_count} > 0` + ), + check( + 'cost_insight_owner_hour_totals_count_safe_check', + sql`${table.spend_record_count} <= 9007199254740991` + ), + ] +); + +export type CostInsightOwnerHourTotal = typeof cost_insight_owner_hour_totals.$inferSelect; + +export const cost_insight_owner_hour_driver_buckets = pgTable( + 'cost_insight_owner_hour_driver_buckets', + { + id: uuid() + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey() + .notNull(), + owned_by_user_id: text().references(() => kilocode_users.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + owned_by_organization_id: uuid().references(() => organizations.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + hour_start: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + spend_category: text().$type().notNull(), + driver_key: text().notNull(), + source: text().$type().notNull(), + product_key: text().notNull(), + feature_key: text().notNull(), + model_or_plan_key: text().notNull(), + provider_key: text().notNull(), + actor_user_id: text() + .notNull() + .references(() => kilocode_users.id, { onUpdate: 'cascade' }), + total_microdollars: bigint({ mode: 'number' }).notNull(), + spend_record_count: bigint({ mode: 'number' }).notNull(), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + uniqueIndex('UQ_cost_insight_driver_buckets_user') + .on(table.owned_by_user_id, table.hour_start, table.spend_category, table.driver_key) + .where(isNull(table.owned_by_organization_id)), + uniqueIndex('UQ_cost_insight_driver_buckets_org') + .on(table.owned_by_organization_id, table.hour_start, table.spend_category, table.driver_key) + .where(isNull(table.owned_by_user_id)), + index('IDX_cost_insight_driver_buckets_hour').on(table.hour_start), + check( + 'cost_insight_driver_buckets_owner_check', + sql`(${table.owned_by_user_id} IS NOT NULL AND ${table.owned_by_organization_id} IS NULL) OR (${table.owned_by_user_id} IS NULL AND ${table.owned_by_organization_id} IS NOT NULL)` + ), + check( + 'cost_insight_driver_buckets_hour_check', + sql`${table.hour_start} = date_trunc('hour', ${table.hour_start}, 'UTC')` + ), + enumCheck( + 'cost_insight_driver_buckets_category_check', + table.spend_category, + CostInsightSpendCategory + ), + enumCheck('cost_insight_driver_buckets_source_check', table.source, CostInsightSpendSource), + check( + 'cost_insight_driver_buckets_driver_key_check', + sql`${table.driver_key} ~ '^[0-9a-f]{64}$'` + ), + check( + 'cost_insight_driver_buckets_product_key_check', + sql`char_length(${table.product_key}) BETWEEN 1 AND 128` + ), + check( + 'cost_insight_driver_buckets_feature_key_check', + sql`char_length(${table.feature_key}) BETWEEN 1 AND 128` + ), + check( + 'cost_insight_driver_buckets_model_key_check', + sql`char_length(${table.model_or_plan_key}) BETWEEN 1 AND 128` + ), + check( + 'cost_insight_driver_buckets_provider_key_check', + sql`char_length(${table.provider_key}) BETWEEN 1 AND 128` + ), + check( + 'cost_insight_driver_buckets_amount_positive_check', + sql`${table.total_microdollars} > 0` + ), + check( + 'cost_insight_driver_buckets_amount_safe_check', + sql`${table.total_microdollars} <= 9007199254740991` + ), + check('cost_insight_driver_buckets_count_positive_check', sql`${table.spend_record_count} > 0`), + check( + 'cost_insight_driver_buckets_count_safe_check', + sql`${table.spend_record_count} <= 9007199254740991` + ), + ] +); + +export type CostInsightOwnerHourDriverBucket = + typeof cost_insight_owner_hour_driver_buckets.$inferSelect; + +export const cost_insight_rollup_coverage = pgTable( + 'cost_insight_rollup_coverage', + { + rollup_version: smallint().primaryKey().notNull(), + live_capture_start_hour: timestamp({ withTimezone: true, mode: 'string' }), + coverage_start_hour: timestamp({ withTimezone: true, mode: 'string' }), + last_reconciled_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + check('cost_insight_rollup_coverage_version_check', sql`${table.rollup_version} > 0`), + check( + 'cost_insight_rollup_coverage_live_hour_check', + sql`${table.live_capture_start_hour} IS NULL OR ${table.live_capture_start_hour} = date_trunc('hour', ${table.live_capture_start_hour}, 'UTC')` + ), + check( + 'cost_insight_rollup_coverage_start_hour_check', + sql`${table.coverage_start_hour} IS NULL OR ${table.coverage_start_hour} = date_trunc('hour', ${table.coverage_start_hour}, 'UTC')` + ), + check( + 'cost_insight_rollup_coverage_range_check', + sql`${table.coverage_start_hour} IS NULL OR (${table.live_capture_start_hour} IS NOT NULL AND ${table.coverage_start_hour} <= ${table.live_capture_start_hour})` + ), + ] +); + +export type CostInsightRollupCoverage = typeof cost_insight_rollup_coverage.$inferSelect; + +export const cost_insight_rollup_degraded_intervals = pgTable( + 'cost_insight_rollup_degraded_intervals', + { + id: uuid() + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey() + .notNull(), + start_hour: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + end_hour_exclusive: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + source: text().$type(), + reason: text().$type().notNull(), + detected_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + resolved_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + index('IDX_cost_insight_degraded_intervals_unresolved') + .on(table.start_hour, table.end_hour_exclusive) + .where(isNull(table.resolved_at)), + check( + 'cost_insight_degraded_intervals_start_hour_check', + sql`${table.start_hour} = date_trunc('hour', ${table.start_hour}, 'UTC')` + ), + check( + 'cost_insight_degraded_intervals_end_hour_check', + sql`${table.end_hour_exclusive} = date_trunc('hour', ${table.end_hour_exclusive}, 'UTC')` + ), + check( + 'cost_insight_degraded_intervals_range_check', + sql`${table.end_hour_exclusive} > ${table.start_hour}` + ), + check( + 'cost_insight_degraded_intervals_resolution_check', + sql`${table.resolved_at} IS NULL OR ${table.resolved_at} >= ${table.detected_at}` + ), + enumCheck('cost_insight_degraded_intervals_source_check', table.source, CostInsightSpendSource), + enumCheck( + 'cost_insight_degraded_intervals_reason_check', + table.reason, + CostInsightRollupDegradedReason + ), + ] +); + +export type CostInsightRollupDegradedInterval = + typeof cost_insight_rollup_degraded_intervals.$inferSelect; + export const organization_memberships = pgTable( 'organization_memberships', { diff --git a/services/kiloclaw-billing/src/lifecycle.test.ts b/services/kiloclaw-billing/src/lifecycle.test.ts index 3b96a55d0b..0cc6938f25 100644 --- a/services/kiloclaw-billing/src/lifecycle.test.ts +++ b/services/kiloclaw-billing/src/lifecycle.test.ts @@ -2,11 +2,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type * as DbModule from '@kilocode/db'; const { + mockCaptureCostInsightSpend, mockFindLatestPreCutoffUserCommitSwitchQualification, mockGetWorkerDb, mockGetMissingSnowflakeConfig, mockQueryKiloclawActiveUserIds, } = vi.hoisted(() => ({ + mockCaptureCostInsightSpend: vi.fn<(tx: unknown, input: { occurredAt: string }) => Promise>( + async () => undefined + ), mockFindLatestPreCutoffUserCommitSwitchQualification: vi.fn< () => Promise >(async () => null), @@ -25,6 +29,12 @@ vi.mock('@kilocode/db', async importOriginal => { }; }); +vi.mock('@kilocode/db/cost-insights-rollups', () => ({ + captureCostInsightSpend: mockCaptureCostInsightSpend, + COST_INSIGHT_DRIVER_FALLBACK: 'other', + COST_INSIGHT_KILOCLAW_PRODUCT_KEY: 'kiloclaw-hosting', +})); + vi.mock('./snowflake.js', () => ({ getMissingSnowflakeConfig: mockGetMissingSnowflakeConfig, queryKiloclawActiveUserIds: mockQueryKiloclawActiveUserIds, @@ -1434,6 +1444,7 @@ describe('credit renewal fanout queue processing', () => { }) ); expect(duplicateResult.credit_renewals_skipped_duplicate).toBe(1); + expect(mockCaptureCostInsightSpend).not.toHaveBeenCalled(); expect(staleResult.credit_renewals).toBe(0); expect(staleResult.credit_renewals_past_due).toBe(0); expect(staleResult.errors).toBe(0); @@ -1572,6 +1583,28 @@ describe('credit renewal fanout queue processing', () => { expect(fetchImpl).not.toHaveBeenCalled(); }); + it('does not mutate balance or subscription when scheduled-spend capture fails', async () => { + const row = creditRenewalRow(); + const { db, txInserts, txUpdates } = createMockDb([[row], [row]]); + mockGetWorkerDb.mockReturnValue(db); + mockCaptureCostInsightSpend.mockRejectedValueOnce(new Error('rollup unavailable')); + + await expect( + processCreditRenewalItem( + createEnvWithQueueMocks(vi.fn()).env, + creditRenewalItemMessage({ renewalBoundary: '2026-06-01T00:00:00.000Z' }), + 1 + ) + ).rejects.toThrow('rollup unavailable'); + + expect(txInserts).toEqual([ + expect.objectContaining({ + credit_category: 'kiloclaw-subscription:22222222-2222-4222-8222-222222222222:2026-06', + }), + ]); + expect(txUpdates).toHaveLength(0); + }); + it('resolves a terminal failure when an operator retry finalizes an expected past-due outcome', async () => { const row = creditRenewalRow({ total_microdollars_acquired: 0, @@ -5954,6 +5987,23 @@ describe('credit renewal sweep affiliate tracking', () => { }), ]) ); + expect(mockCaptureCostInsightSpend).toHaveBeenCalledWith(expect.anything(), { + owner: { type: 'user', id: 'user-1' }, + actorUserId: 'user-1', + occurredAt: expect.any(String), + amountMicrodollars: 9_000_000, + category: 'scheduled', + source: 'kiloclaw', + productKey: 'kiloclaw-hosting', + featureKey: 'renewal', + modelOrPlanKey: 'standard', + providerKey: 'other', + }); + const captureInput = mockCaptureCostInsightSpend.mock.calls[0]?.[1] as + | { occurredAt: string } + | undefined; + const deduction = txInserts.find(insert => insert.amount_microdollars === -9_000_000); + expect(deduction?.created_at).toBe(captureInput?.occurredAt); const saleCall = fetch.mock.calls .map( diff --git a/services/kiloclaw-billing/src/lifecycle.ts b/services/kiloclaw-billing/src/lifecycle.ts index 52411c7283..39291cd3ba 100644 --- a/services/kiloclaw-billing/src/lifecycle.ts +++ b/services/kiloclaw-billing/src/lifecycle.ts @@ -24,6 +24,11 @@ import { type OrganizationSeatsPurchase, } from '@kilocode/db'; import { classifyOrganizationEntitlement } from '@kilocode/organization-entitlement'; +import { + captureCostInsightSpend, + COST_INSIGHT_DRIVER_FALLBACK, + COST_INSIGHT_KILOCLAW_PRODUCT_KEY, +} from '@kilocode/db/cost-insights-rollups'; import { listOrganizationTrialExpiryEnforcementCandidates, type OrganizationTrialExpiryCandidateRow, @@ -2024,6 +2029,7 @@ async function processCreditRenewalRow( const newPeriodEnd = addMonths(new Date(renewalAt), periodMonths).toISOString(); const wasPastDue = current.status === 'past_due'; const beforeSubscription = await getSubscriptionById(tx, current.id); + const occurredAt = new Date().toISOString(); const deductionResult = await tx .insert(credit_transactions) .values({ @@ -2035,6 +2041,7 @@ async function processCreditRenewalRow( credit_category: deductionCategory, check_category_uniqueness: true, original_baseline_microdollars_used: current.microdollars_used, + created_at: occurredAt, }) .onConflictDoNothing(); @@ -2099,6 +2106,19 @@ async function processCreditRenewalRow( } satisfies CreditRenewalTransactionOutcome; } + await captureCostInsightSpend(tx, { + owner: { type: 'user', id: userId }, + actorUserId: userId, + occurredAt, + amountMicrodollars: costMicrodollars, + category: 'scheduled', + source: 'kiloclaw', + productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, + featureKey: 'renewal', + modelOrPlanKey: effectivePlan, + providerKey: COST_INSIGHT_DRIVER_FALLBACK, + }); + await tx .update(kilocode_users) .set({ From c0c1793c75df9eeb7a834bcdcb00b8ba4cbc7f63 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Thu, 25 Jun 2026 21:31:01 +0200 Subject: [PATCH 05/11] feat(cost-insight): implement cost insights UI --- .plans/cost-insights-data-layer.md | 327 +++++----- .plans/cost-insights.md | 145 +++-- .specs/cost-insights.md | 4 +- CONTEXT.md | 6 +- .../cost-insights/Settings.stories.tsx | 2 +- .../components/OrganizationAppSidebar.tsx | 11 + .../(app)/components/PersonalAppSidebar.tsx | 9 +- .../app/(app)/cost-insights/activity/page.tsx | 4 +- .../app/(app)/cost-insights/ask-kilo/page.tsx | 17 +- .../app/(app)/cost-insights/config/page.tsx | 5 + apps/web/src/app/(app)/cost-insights/page.tsx | 4 +- .../app/(app)/cost-insights/settings/page.tsx | 4 +- .../[id]/cost-insights/activity/page.tsx | 13 +- .../[id]/cost-insights/ask-kilo/page.tsx | 18 +- .../[id]/cost-insights/config/page.tsx | 12 + .../organizations/[id]/cost-insights/page.tsx | 18 +- .../[id]/cost-insights/settings/page.tsx | 13 +- .../api/cron/cost-insights-hourly/route.ts | 36 ++ .../api/cron/cost-insights-retention/route.ts | 36 ++ .../CostInsightsActivityClient.tsx | 34 + .../cost-insights/CostInsightsLayout.tsx | 2 +- .../CostInsightsOverviewClient.tsx | 149 +++++ .../CostInsightsRoutePlaceholder.tsx | 18 - .../CostInsightsSettingsClient.tsx | 166 +++++ .../ask-kilo/CostInsightsAskKiloView.tsx | 38 +- .../web/src/components/cost-insights/index.ts | 3 +- .../cost-insights/overview/AskKiloInput.tsx | 17 +- .../overview/CostInsightsDashboardView.tsx | 38 +- .../overview/DashboardNotices.tsx | 30 +- .../settings/CostInsightsSettingsView.tsx | 29 +- .../shell/CostInsightsShellView.tsx | 6 +- .../web/src/components/cost-insights/types.ts | 9 +- apps/web/src/emails/AGENTS.md | 1 + .../web/src/emails/costInsightSpendAlert.html | 86 +++ apps/web/src/lib/ai-gateway/processUsage.ts | 8 + .../coding-plans/billing-lifecycle-cron.ts | 7 +- apps/web/src/lib/coding-plans/index.ts | 2 + apps/web/src/lib/cost-insights/evaluation.ts | 389 ++++++++++++ apps/web/src/lib/cost-insights/jobs.ts | 49 ++ .../src/lib/cost-insights/notifications.ts | 221 +++++++ apps/web/src/lib/cost-insights/owner.ts | 78 +++ apps/web/src/lib/cost-insights/policy.test.ts | 63 ++ apps/web/src/lib/cost-insights/policy.ts | 103 +++ apps/web/src/lib/cost-insights/presenter.ts | 441 +++++++++++++ apps/web/src/lib/cost-insights/repository.ts | 591 ++++++++++++++++++ apps/web/src/lib/email.ts | 26 + apps/web/src/lib/exa-usage.ts | 6 + apps/web/src/lib/kiloclaw/credit-billing.ts | 2 + apps/web/src/routers/cost-insights-router.ts | 208 ++++++ .../organization-cost-insights-router.ts | 161 +++++ .../organizations/organization-router.ts | 2 + apps/web/src/routers/root-router.ts | 2 + apps/web/vercel.json | 8 + dev/seed/cost-insights/spend-evidence.ts | 465 +++++++++++++- packages/db/src/schema-types.ts | 38 ++ packages/db/src/schema.ts | 387 ++++++++++++ 56 files changed, 4246 insertions(+), 321 deletions(-) create mode 100644 apps/web/src/app/(app)/cost-insights/config/page.tsx create mode 100644 apps/web/src/app/(app)/organizations/[id]/cost-insights/config/page.tsx create mode 100644 apps/web/src/app/api/cron/cost-insights-hourly/route.ts create mode 100644 apps/web/src/app/api/cron/cost-insights-retention/route.ts create mode 100644 apps/web/src/components/cost-insights/CostInsightsActivityClient.tsx create mode 100644 apps/web/src/components/cost-insights/CostInsightsOverviewClient.tsx delete mode 100644 apps/web/src/components/cost-insights/CostInsightsRoutePlaceholder.tsx create mode 100644 apps/web/src/components/cost-insights/CostInsightsSettingsClient.tsx create mode 100644 apps/web/src/emails/costInsightSpendAlert.html create mode 100644 apps/web/src/lib/cost-insights/evaluation.ts create mode 100644 apps/web/src/lib/cost-insights/jobs.ts create mode 100644 apps/web/src/lib/cost-insights/notifications.ts create mode 100644 apps/web/src/lib/cost-insights/owner.ts create mode 100644 apps/web/src/lib/cost-insights/policy.test.ts create mode 100644 apps/web/src/lib/cost-insights/policy.ts create mode 100644 apps/web/src/lib/cost-insights/presenter.ts create mode 100644 apps/web/src/lib/cost-insights/repository.ts create mode 100644 apps/web/src/routers/cost-insights-router.ts create mode 100644 apps/web/src/routers/organizations/organization-cost-insights-router.ts diff --git a/.plans/cost-insights-data-layer.md b/.plans/cost-insights-data-layer.md index 8470955c07..2148766ae5 100644 --- a/.plans/cost-insights-data-layer.md +++ b/.plans/cost-insights-data-layer.md @@ -2,9 +2,22 @@ ## Status -Ready for implementation. This plan covers Credit-spend capture, owner-hour rollups, read repositories, historical backfill, repair, and rollout validation. Alert evaluation, Cost Insight Events, notifications, tRPC routes, and UI are follow-on work. +Implemented in commit `f060ef557` and validated against local PostgreSQL. Slices 0 through 5 are complete in code. Slice 6 has working canonical aggregation, backfill, targeted repair, reconciliation, coverage, degraded-interval handling, and operator scripts, but production execution remains pending. -The business rules remain in `.specs/cost-insights.md`. Canonical terminology remains in `CONTEXT.md`. The broader feature sequence remains in `.plans/cost-insights.md`. +Do not mark this plan complete until production EXPLAINs, capture latency and lock-contention benchmarks, production Exa historical partition indexes, coordinated web/Worker cutover, contiguous 7-day and 90-day backfill, canary reconciliation, deployment-boundary reconciliation, and production observability are complete. + +Alert evaluation, Cost Insight Events, notifications, tRPC routes, and UI are implemented in the follow-on Cost Insights slice described in `.plans/cost-insights.md`. Business rules remain in `.specs/cost-insights.md`; canonical terminology remains in `CONTEXT.md`. + +## Implemented result + +- Generated migration `packages/db/src/migrations/0173_workable_carlie_cooper.sql` adds four unpartitioned Spend evidence tables with owner, UTC-hour, taxonomy, amount, count, coverage, and degraded-interval constraints. +- `@kilocode/db/cost-insights-rollups` provides validated UTC bucketing, normalized driver dimensions, versioned SHA-256 driver keys, owner-hour advisory locking, and one-statement additive total/driver capture. +- AI Gateway, charged Exa, Coding Plan activation/renewal, and pure-credit KiloClaw enrollment/renewal capture Variable or Scheduled Credit spend in the same transaction as their source and balance mutations. +- AI Gateway, charged Exa, Coding Plan activation/renewal, and pure-credit KiloClaw enrollment schedule Cost Insights evaluation after web transactions commit. KiloClaw Worker renewal captures are evaluated by the hourly Cost Insights sweep. +- `apps/web/src/lib/cost-insights/spend-repository.ts` provides dense hourly reads, current-hour totals, top drivers, coverage state, and exact rolling `[asOf - 24h, asOf)` reads. +- `apps/web/src/lib/cost-insights/canonical-sources.ts` and `rollup-maintenance.ts` provide four-source canonical aggregation, bounded absolute replacement, owner repair, reconciliation, and degraded-interval workflows. +- `apps/web/src/scripts/db/cost-insights-rollups.ts` defaults to dry-run reconciliation and requires explicit bounded execution. `exa-usage-log-indexes.ts` performs bounded, newest-first partition-local Exa index rollout. +- `dev/seed/cost-insights/spend-evidence.ts` creates local-only personal and organization fixtures with 90 days of sparse evidence, current-hour spikes, Scheduled spend, and member driver attribution. ## Goal @@ -31,7 +44,7 @@ Every production operation that increments `microdollars_used` must update this ### Storage shape -Add four unpartitioned tables: +The implementation adds four unpartitioned tables: 1. `cost_insight_owner_hour_totals` 2. `cost_insight_owner_hour_driver_buckets` @@ -89,7 +102,7 @@ Use the source spend timestamp, not processing or backfill time. Normalize bucke ### Controlled schema values -Add `CostInsightSpendCategory` and `CostInsightSpendSource` runtime/type values to `packages/db/src/schema-types.ts`. Register them in `SCHEMA_CHECK_ENUMS` and enforce them through `enumCheck` constraints in `packages/db/src/schema.ts`. +`CostInsightSpendCategory`, `CostInsightSpendSource`, and `CostInsightRollupDegradedReason` are registered in `SCHEMA_CHECK_ENUMS` and enforced through `enumCheck` constraints in `packages/db/src/schema.ts`. ### Owner-hour totals @@ -185,7 +198,7 @@ Create a degraded interval before an intentional capture bypass, and immediately ## Shared capture module -Add a subpath-only export `@kilocode/db/cost-insights-rollups`, backed by `packages/db/src/cost-insights-rollups.ts` and exported through `packages/db/package.json`. Do not add it to the broad root barrel; explicit imports keep the billing-critical persistence boundary visible in web and Worker call sites. +The subpath-only export `@kilocode/db/cost-insights-rollups` is backed by `packages/db/src/cost-insights-rollups.ts` and exported through `packages/db/package.json`. It is not re-exported from the broad root barrel, so the billing-critical persistence boundary remains visible in web and Worker call sites. The module owns: @@ -194,7 +207,8 @@ The module owns: - Explicit UTC bucket calculation. - Transaction-scoped owner-hour advisory locking. - Total and driver additive upserts in a fixed lock order. -- Generic owner-range read helpers that do not format USD or resolve labels. + +Generic owner-range reads live in `apps/web/src/lib/cost-insights/spend-repository.ts`. Keeping them out of the shared package avoids pulling application read policy into the billing-critical web/Worker capture boundary. Suggested input: @@ -228,7 +242,7 @@ type CaptureCostInsightSpendInput = { 6. Upsert the combined driver bucket. 7. Increment amount and count and set `updated_at = now()` explicitly. -Every writer and targeted repair acquires the same owner-hour advisory lock. This prevents an absolute repair from overwriting a concurrent live contribution. Always lock total before driver to avoid lock-order drift. +The implemented helper performs lock acquisition, total upsert, driver upsert, and digest-collision outcome reporting in one SQL statement. Every writer and targeted repair acquires the same owner-hour advisory lock. This prevents an absolute repair from overwriting a concurrent live contribution. Total mutation precedes driver mutation in the statement. The module must accept a structural transaction writer type. It must not import the web database singleton, create a Worker database client, or cache transport-owning state. KiloClaw Worker continues creating request-scoped clients through `getWorkerDb`. @@ -425,24 +439,22 @@ The existing daily rollup is a secondary checksum, not a backfill source. It lac ### Shared canonical mapping -Add source-specific canonical aggregation functions under `apps/web/src/lib/cost-insights/`. Live and historical mapping must share constants for category, source, product, feature, and fallback values. - -Backfill SQL may need source-specific `CASE` expressions for set-based aggregation. Add parity fixtures proving live mapping and historical mapping produce the same driver keys for representative source rows. +Source-specific canonical aggregation lives in `apps/web/src/lib/cost-insights/canonical-sources.ts`. Live and historical mapping share constants for category, source, product, feature, and fallback values. Mapper fixtures cover representative values, while full real-source/live-digest parity remains a test gap. -Historical gaps map to `other`; do not infer mutable event-time data from current subscriptions or user profiles. +Historical gaps map to `other`; the implementation does not infer mutable event-time data from current subscriptions or user profiles. ### Bulk backfill script -Add an operator script under `apps/web/src/scripts/` and register it in the existing script index. It must default to dry run and require explicit execution. +The operator script at `apps/web/src/scripts/db/cost-insights-rollups.ts` is auto-discovered by the existing script runner. It defaults to dry-run reconciliation and requires explicit execution for writes. + +Implemented parameters: -Parameters: +- `--execute` to enable mutation; omission runs reconciliation only. +- Required `--start-hour`, `--end-hour`, and `--max-hours` bounds. +- Optional `--sleep-ms` pacing. +- Optional one-time `--live-capture-start-hour` coverage initialization. -- `--execute` -- `--start-hour` -- `--end-hour` -- `--max-hours` -- `--sleep-ms` -- Optional source/owner diagnostics without changing mapping semantics. +Source/owner diagnostic filters and a targeted-repair command are not implemented. Add them only if they retain the same canonical mapping and bounded execution rules. Process newest completed hours first so every successful step extends one contiguous interval backward from live capture: @@ -454,24 +466,24 @@ Process newest completed hours first so every successful step extends one contig Before execution, run `EXPLAIN` against production-shaped data for every source query. Confirm `microdollar_usage.created_at` range scans, Exa partition pruning, and bounded credit-transaction/term scans. Do not add or replace indexes on the large raw usage table without a separate online-index rollout plan. -For each completed pre-cutover hour: +The implemented operator processes only completed pre-cutover hours and uses this shape: -1. Aggregate all four canonical source families into temporary/staging results using half-open timestamp predicates. -2. Build owner/category totals and owner/category/source/driver buckets. -3. In one bounded `REPEATABLE READ` transaction, delete existing aggregate rows for that hour and insert absolute staged results. -4. Verify owner totals equal the sum of driver amounts and counts. -5. Commit. -6. Move `coverage_start_hour` back only when the new hour is contiguous with existing coverage. +1. Load up to 24 hours from all four canonical source families in one read-only `REPEATABLE READ` snapshot with half-open predicates. +2. Build owner/category totals and owner/category/source/driver buckets in memory. +3. Process hours newest-first. For each hour, delete existing aggregate rows and insert absolute staged results in its own bounded transaction. +4. Verify owner totals equal driver amounts and counts. +5. Move `coverage_start_hour` backward only when the hour is contiguous with existing coverage. +6. Reconcile the full requested range after execution. A mismatch records an unresolved degraded interval and fails the execute run. -Absolute replacement makes reruns safe. Never reuse the live additive `total = total + excluded.total` behavior for backfill. Pre-cutover ranges run only after normal async persistence has drained; overlapping or unexpectedly late owner-hours use targeted advisory-locked repair instead of a global serializable transaction. +The 24-hour source chunk avoids hourly source-query amplification while keeping writes and coverage advancement hour-sized. Absolute replacement makes reruns safe. The global path refuses live or post-cutover hours; overlapping or late owner-hours use advisory-locked targeted repair. -Start with one hour per source scan and no concurrency. After benchmarks, permit a bounded multi-hour staging scan only if it reduces repeated raw-table IO while keeping replacement transactions and coverage advancement small. Bound statement and lock timeouts. Stop on elevated database load, replication lag, lock waits, or reconciliation differences. +Production use must still set practical statement/lock limits and monitor database load, WAL, replication lag, lock waits, and reconciliation differences. The script does not stop automatically from those telemetry signals. -The deployment boundary and preceding hour need a second reconciliation pass after normal async usage persistence has drained. If a later source row exposes a gap, create a degraded interval first, repair affected owner-hours, reconcile the interval, then resolve it. +The deployment boundary and preceding hour need a second reconciliation pass after deferred usage persistence drains. If a later source row exposes a gap, create a degraded interval first, repair affected owner-hours, reconcile the interval, then resolve it. Current library functions support this lifecycle, but no single operator command performs targeted repair plus safe resolution. ### Targeted owner repair -Add `repairOwnerSpendRollups(owner, startHour, endHourExclusive)` with an explicit maximum supplied by the caller. Future Spend Alert enablement uses a hard seven-day cap. Operator repair may use up to 90 days with lower concurrency and stricter timeouts. +`repairOwnerSpendRollups(owner, startHour, endHourExclusive)` is implemented with an explicit caller-supplied maximum. Future Spend Alert enablement must use a hard seven-day cap. Operator repair may use up to 90 days with lower concurrency and stricter timeouts, but no targeted-repair CLI is available yet. For each owner-hour: @@ -485,7 +497,7 @@ Delete aggregate rows when the canonical result is zero. The repair path must be ### Reconciliation -Add a dry-run reconciliation mode that compares rollups with canonical source sums for bounded owner/hour samples and reports: +Dry-run reconciliation compares rollups with canonical source sums for bounded hour ranges and reports: - Missing totals. - Amount differences. @@ -498,124 +510,109 @@ Run canaries for high-volume organizations, normal personal users, Exa users, Ki ## Observability -Instrument capture by source without logging sensitive request data: - -- Capture latency. -- Total and driver upsert failures. -- Advisory-lock wait duration. -- Transaction rollback count. -- Rows and microdollars captured by source/category. -- Backfill hour duration and staged row counts. -- Reconciliation mismatch count and amount. -- Coverage start and age. -- Unresolved degraded-interval count and age. -- Exact rolling-24h boundary-fragment query latency. - -Add Sentry context with source, category, owner type, and source record ID where available. Do not attach prompts, auth headers, cookies, tokens, Exa request bodies, user email, or display name. - -Monitor database tuple/advisory lock waits, WAL volume, index growth, autovacuum lag, replica lag, and AI Gateway/Exa persistence latency through existing database telemetry. - -## Tests - -### Schema and helper tests - -- Exactly-one-owner constraints. -- Controlled category/source constraints. -- UTC-hour normalization across session timezones and DST boundaries. -- Personal and organization uniqueness. -- Driver fallback normalization, length bounds, deterministic digest, and collision mismatch failure. -- Additive total and driver updates. -- Amount and record-count overflow/unsafe-integer rejection. -- Forced driver failure rolls back total and source spend transaction. -- Concurrent updates produce exact sums. -- Same owner/hour repair and live capture do not lose a contribution. - -### Source integration tests - -- AI personal positive spend updates raw usage, daily rollup, balance, total, and driver atomically. -- AI organization positive spend updates raw usage, organization balance, member daily usage, total, and driver atomically. -- AI zero-cost/BYOK rows create no Cost Insights rows. -- Charged Exa personal and organization requests produce Variable spend. -- Exa free allowance produces no Cost Insights spend. -- Coding Plan activation and renewal produce Scheduled spend once. -- KiloClaw enrollment and Worker renewal produce Scheduled spend once. -- Duplicate KiloClaw/Coding Plan paths do not increment rollups. -- KiloClaw settlement, credit grants, top-ups, expirations, refunds, and accounting adjustments produce no rollups. -- Rollup failure prevents the corresponding charge/source transaction from committing. - -### Read tests - -- 24h, 7d, 30d, and 90d queries return exact UTC bucket counts. -- Covered missing hours return zero. -- Uncovered and degraded hours remain marked unknown. -- Category totals equal bucket totals. -- Exact rolling-24h reads combine full rollup hours and raw boundary fragments without double counting. -- Top drivers aggregate combined dimensions and use deterministic tie-breaking. -- Personal and organization owner data cannot cross scopes. - -### Backfill and repair tests - -- Canonical source fixtures map to the same category/source/driver values as live capture. -- Backfill rerun produces identical totals and drivers. -- Failed hour does not move coverage. -- Empty source hour advances coverage without aggregate rows. -- Unresolved degraded intervals suppress zero-fill until repair and reconciliation resolve them. -- Targeted repair corrects missing and inflated rows. -- Targeted repair deletes rows whose canonical source sum is zero. -- Concurrent late source contribution plus repair is counted exactly once. -- Historical unknown fields use `other` rather than mutable current values. - -## Delivery sequence - -### Slice 0: spend-writer audit - -Repeat the direct balance-mutation audit, classify every production mutation, and fail planning/implementation review on unexplained writers. Record exclusions and canonical source identity in tests or repository-local code comments next to the central capture contract. - -Outcome: implementation has a closed producer inventory before it claims complete coverage. - -### Slice 1: schema and capture primitive - -Files: - -- `packages/db/src/schema-types.ts` -- `packages/db/src/schema.ts` -- `packages/db/src/cost-insights-rollups.ts` -- `packages/db/package.json` -- Generated migration and schema/helper tests - -Outcome: tables and transaction-bound capture helper exist, with no producer calling them yet. - -Generate migration with `pnpm drizzle generate`. Do not hand-write or edit generated migration SQL, snapshot, or journal. - -### Slice 2: already-transactional scheduled spend - -Integrate Coding Plan activation/renewal and KiloClaw web/Worker charge paths. These paths need the least transaction restructuring and validate the shared helper in both Next.js and Cloudflare Worker environments. - -Outcome: all Scheduled Credit spend dual-writes atomically. - -### Slice 3: AI Gateway transaction consolidation - -Refactor personal and organization AI persistence into one caller-owned transaction, add capture, and move organization low-balance email scheduling after commit. - -Outcome: AI Gateway Variable Credit spend dual-writes atomically for both owner types. - -### Slice 4: Exa transaction consolidation - -Combine Exa log, monthly counter, owner charge, organization member usage, and capture in one transaction. - -Outcome: all known Variable Credit spend paths dual-write atomically. - -### Slice 5: read repository and coverage - -Implement dense hourly evidence, current-hour totals, exact rolling-24h composition, top drivers, coverage, and degraded-interval reads. - -Outcome: application work can consume one Postgres datasource without knowing source ledgers. - -### Slice 6: backfill, repair, and shadow validation - -Implement canonical aggregation, dry-run reconciliation, 7-day then 90-day backfill, and targeted owner repair. - -Outcome: coverage reaches 90 days with zero reconciliation differences before alerts or dashboard data rely on it. +Current operator output reports backfill hour duration, staged total/driver/source counts, canonical microdollars, reconciliation mismatch classes, and coverage advancement. AI Gateway capture failures include bounded Cost Insights context without request payloads. + +Production instrumentation still needs: + +- Capture latency by source and category. +- Total/driver upsert failure and transaction rollback counts. +- Advisory-lock wait duration and contention. +- Captured rows and microdollars by source/category. +- Reconciliation mismatch amount, not only count/class. +- Coverage age and unresolved degraded-interval count/age. +- Exact rolling-24-hour boundary query latency. + +Do not attach prompts, auth headers, cookies, tokens, Exa request bodies, user email, or display name. Monitor tuple/advisory lock waits, WAL volume, index growth, autovacuum lag, replica lag, and AI Gateway/Exa persistence latency through existing database telemetry during rollout. + +## Test status + +### Schema and capture + +| Coverage | Status | Remaining | +|---|---|---| +| Exactly-one-owner, controlled values, UTC normalization, owner isolation | Done | Add direct duplicate-insert assertions for both partial unique indexes if schema regression coverage needs to be stricter | +| Driver fallback, bounds, digest determinism, collision rollback | Done | None for v1 contract | +| Additive totals/drivers, unsafe input rejection, concurrent exact sums | Done | Add existing-row additive overflow coverage near JavaScript safe-integer limit | +| Repair and live capture on same owner-hour | Done | None | + +### Producer integration + +| Coverage | Status | Remaining | +|---|---|---| +| AI personal source, metadata, daily aggregate, balance, total/driver, rollback | Done | Add explicit Coding Plan-backed BYOK no-spend fixture | +| AI organization source, balance, member usage, rollup | Partial | Add focused driver-dimension and forced organization rollback assertions | +| Charged/free Exa personal and organization behavior | Done | None for current mapping | +| Coding Plan activation and renewal | Partial | Existing tests verify capture inputs and rollback with a mocked helper; add real-rollup integration coverage | +| KiloClaw enrollment and Worker renewal | Partial | Existing tests verify idempotency/capture ordering with mocked helper; add real-rollup integration coverage | +| Explicit accounting exclusions | Partial | Add focused proof that settlement, grants, top-ups, expiration, refunds, and adjustments leave rollups unchanged | + +### Reads and maintenance + +| Coverage | Status | Remaining | +|---|---|---| +| Covered zero versus uncovered/degraded unknown | Done | None | +| Exact rolling 24h with raw boundary fragments | Done | Add production-shaped latency benchmark | +| Preset ranges | Partial | Add exact 24h, 7d, 30d, and 90d bucket-count tests | +| Top drivers | Partial | Add deterministic tie ordering, category filter, and organization-scope integration tests | +| Canonical mapping parity | Partial | Pure mapping fixtures cover all sources; add real-source/live-digest parity fixtures for all four families | +| Backfill rerun and zero-source deletion | Done | None | +| Coverage advancement failures and empty hours | Remaining | Add failed-hour no-advance and empty-hour advance tests | +| Targeted repair | Partial | Add nonzero missing/inflated correction; zero deletion and live-concurrency behavior are covered | +| Degraded lifecycle | Partial | Suppression and resolution are covered separately; add full record, repair, reconcile, resolve test | + +## Delivery status + +| Slice | Status | Result or remaining gate | +|---|---|---| +| 0. Spend-writer audit | Done | Current production increments are classified and guarded by `spend-writer-audit.test.ts`; keep the regex inventory updated for new mutation forms | +| 1. Schema and capture primitive | Done | Four tables, controlled values, generated migration, subpath export, capture helper, and database tests exist | +| 2. Scheduled spend | Done in code | Coding Plan and KiloClaw web/Worker paths dual-write atomically; real-rollup producer integration tests remain desirable | +| 3. AI Gateway consolidation | Done | Personal/organization source, charge, daily usage, and rollup share one transaction; low-balance scheduling occurs after commit | +| 4. Exa consolidation | Done | Log, monthly counter, charge, member usage, and rollup share one transaction | +| 5. Read repository and coverage | Done in code | Dense hourly, current hour, top drivers, exact rolling 24h, coverage, and degraded reads exist; preset/tie/scope test gaps remain | +| 6. Backfill, repair, shadow validation | Partial | Code and local validation are complete; production indexes, EXPLAINs, cutover, 7-day/90-day runs, canaries, boundary reconciliation, and observability remain | + +## Production rollout checklist + +- [ ] Deploy migration before mandatory capture code can execute. +- [ ] Roll out historical Exa partition indexes with an explicit small `--max-partitions` bound; verify created indexes are valid and ready. +- [ ] Run production-shaped EXPLAINs for AI Gateway, Exa, Coding Plan, and KiloClaw canonical queries. +- [ ] Benchmark capture-enabled AI Gateway and Exa persistence at production-shaped concurrency; record p50/p95/p99 latency and advisory-lock waits. +- [ ] Coordinate web and Worker deployments, then choose the first full UTC `live_capture_start_hour` after all writers are active. +- [ ] Let deferred AI Gateway/Exa persistence drain before replacing pre-cutover hours. +- [ ] Backfill and reconcile the newest contiguous seven days before anomaly evaluation uses baseline history. +- [ ] Continue contiguous backfill and reconciliation to 90 days before 30d/90d UI ranges claim complete evidence. +- [ ] Run canaries for high-volume organizations, ordinary personal owners, Exa, KiloClaw, and Coding Plan usage. +- [ ] Reconcile cutover hour and preceding hour again after deferred persistence drains. +- [ ] Monitor database load, WAL, replication lag, lock waits, index growth, autovacuum, capture latency, coverage age, and degraded intervals. +- [ ] Document or add a bounded operator workflow for targeted repair and degraded-interval resolution. + +## Operator and local seed notes + +`apps/web/src/scripts/db/exa-usage-log-indexes.ts` creates two partial indexes per historical Exa leaf partition with `CREATE INDEX CONCURRENTLY IF NOT EXISTS`. Future partitions receive equivalent indexes during provisioning. Production runs should always set a small `--max-partitions`; the script does not currently verify `pg_index.indisvalid`/`indisready` after a failed concurrent build. + +`dev/seed/cost-insights/spend-evidence.ts` is local-only and refuses production or non-loopback database targets. It seeds dedicated personal and organization owners using AI Gateway and KiloClaw canonical rows, then writes matching rollups through the production capture helper. It does not seed Exa or Coding Plan. It also extends global local coverage, so use it on a disposable/local database rather than a clone where unrelated canonical rows may lack rollups. Fixture users have placeholder Stripe IDs and should not be used on Stripe-backed pages. + +## Verification completed + +Local implementation validation recorded before commit: + +- 389 web and Cost Insights tests passed. +- 41 database/schema tests passed. +- 185 KiloClaw Worker tests passed. +- Targeted web, database, and Worker typechecks and lint passed. +- `pnpm format`, `git diff --check`, and `pnpm drizzle:verify-bootstrap` passed. +- Full monorepo typecheck was skipped under repository guidance. + +Local operator and seed validation: + +- Four Exa partial indexes were created across two local historical partitions and rerun idempotently. +- Migration tables and journal entry were confirmed in local PostgreSQL. Drizzle CLI returned exit 1 after applying the migration, so production migration execution still needs normal deployment verification. +- One completed empty pre-seed hour was backfilled and reconciled with zero mismatches. +- The dev seed ran twice with identical results: 374 Variable records and 6 Scheduled records. +- All 2,160 seeded hourly buckets reconciled with zero mismatches and zero coverage holes; canonical and rollup totals matched for both fixture owners. + +These results prove local behavior, not production rollout or performance acceptance. ## Verification commands @@ -645,17 +642,21 @@ Benchmark AI Gateway capture against production-shaped concurrency using the exi ## Acceptance criteria -- Every production `microdollars_used` increment has an explicit included/excluded classification. -- AI Gateway, charged Exa, pure-credit KiloClaw, and Coding Plan spend update totals and one driver bucket atomically with the charge. -- Snowflake is absent from capture, reads used for correctness, repair, and backfill. -- A failed mandatory rollup write rolls back its source spend transaction. -- Duplicate billing attempts do not duplicate rollups. -- Hour keys are explicit UTC hours. -- Personal and organization totals cannot collide or leak across scopes. -- Covered zero-spend hours are distinguishable from unknown or degraded history. -- Exact rolling-24h reads use rollup interiors plus canonical raw boundary fragments. -- Newest 7 days reconcile before anomaly work starts; full 90 days reconcile before 30d/90d dashboard evidence is treated as complete. -- Bootstrapped and newly captured rollup rows have no retention expiry. -- Backfill and targeted repair are absolute, idempotent, bounded, and resumable. -- No email, display name, secret, prompt, or arbitrary request payload is persisted in rollup tables. -- Capture latency and lock contention stay within limits agreed from benchmark results. +| Criterion | Status | +|---|---| +| Every current production `microdollars_used` increment has an included/excluded classification | Met; keep audit guard current | +| AI Gateway, charged Exa, pure-credit KiloClaw, and Coding Plan spend atomically update totals and a driver bucket | Met in implementation | +| Snowflake is absent from capture, correctness reads, repair, and backfill | Met | +| Mandatory rollup failure rolls back source spend transaction | Met | +| Duplicate billing attempts do not duplicate rollups | Met in implementation; real-rollup producer integration proof is partial | +| Hour keys are explicit UTC hours | Met | +| Personal and organization totals cannot collide or leak across scopes | Met at schema/repository level | +| Covered zero-spend hours differ from unknown or degraded history | Met | +| Exact rolling 24h uses rollup interiors plus canonical raw boundaries | Met | +| Newest seven production days reconcile before anomaly work starts | Pending production rollout | +| Full 90 production days reconcile before 30d/90d evidence claims completeness | Pending production rollout | +| Bootstrapped and newly captured rollups have no retention expiry | Met | +| Backfill and targeted repair are absolute, idempotent, bounded, and resumable | Met in implementation | +| Rollups contain no email, display name, secret, prompt, or arbitrary request payload | Met | +| Capture latency and lock contention meet agreed limits | Pending production-shaped benchmark | +| Production telemetry detects capture failures, contention, stale coverage, and degraded intervals | Pending | diff --git a/.plans/cost-insights.md b/.plans/cost-insights.md index 74f2fece22..48cd4023f9 100644 --- a/.plans/cost-insights.md +++ b/.plans/cost-insights.md @@ -4,11 +4,20 @@ This plan covers Cost Insights v1, including Spend Alerts and Cost Suggestions. ## Status -Draft plan. Core product decisions are confirmed. Storybook UI exists as design reference. Backend implementation has not started. +Implemented in this branch. The Spend evidence data layer was implemented in commit `f060ef557`; this follow-on slice adds Spend Alert and Cost Suggestion config, owner state, events, notification delivery, evaluation, tRPC procedures, live UI wiring, sidebar attention, email templates, cron jobs, and retention. + +Current state: + +- Credit-spend capture, owner-hour rollups, canonical Postgres reads, coverage, degraded intervals, exact rolling-24-hour reads, backfill, repair, reconciliation, operator scripts, and a local dev seed are implemented. +- Spend Alert and Cost Suggestion config, owner state, active suggestions, Cost Insight Events, notification deliveries, alert/suggestion evaluation, hourly sweep, and 90-day retention cleanup are implemented. +- Personal and organization routes are wired to live tRPC data and mutations. `/config` is the settings route; old `/settings` paths redirect. Ask Kilo v1 routes render UI-only conversation controls without processing questions. +- Cost Insights sidebar placement is directly below Usage. Sidebar attention uses the lightweight unreviewed-alert endpoint. +- Post-commit evaluation scheduling is wired for web spend paths: AI Gateway, charged Exa, Coding Plan activation and renewal, and pure-credit KiloClaw enrollment. KiloClaw Worker renewal captures remain covered by the hourly evaluation sweep. +- Production rollout is not complete. Migration deployment, coordinated web/Worker cutover, production EXPLAINs, latency and lock benchmarks, Exa historical index rollout, contiguous 7-day then 90-day reconciliation, and live notification smoke tests remain operational gates. ## Confirmed Decisions -- Cost Insights is account-level surface for Spend Alerts. +- Cost Insights is the Usage-adjacent surface for Spend Alerts. - Spend Alerts are opt-in for personal users and organizations. - Spend Alerts are alert-only. - Spend Alerts must not block spend, pause usage, throttle usage, suppress auto-top-up, reject paid requests, or emit Spend Alerts-specific HTTP 402 responses. @@ -87,63 +96,81 @@ Draft plan. Core product decisions are confirmed. Storybook UI exists as design - Data-layer implementation and rollout follow `.plans/cost-insights-data-layer.md`; physical schema, locking, driver-key, source-mapping, backfill, and repair mechanics remain there rather than being duplicated in this plan. - Implementation should ship as independently verifiable vertical slices. -## Vertical Slices +## Vertical slices + +The spend-writer audit prerequisite is complete and guarded by `apps/web/src/lib/cost-insights/spend-writer-audit.test.ts`. + +| Slice | Status | Implemented | Remaining | +|---|---|---|---| +| 1. Schema and policy primitives | Implemented | Owner-hour totals, driver buckets, coverage, degraded intervals, config, owner episode state, active suggestions, Cost Insight Events, notification delivery, and pure policy helpers | Production migration deployment | +| 2. Spend evidence data layer | Implemented locally; production rollout pending | Atomic capture for AI Gateway, charged Exa, Coding Plan, and pure-credit KiloClaw; dense hourly reads; exact rolling 24h; top drivers; canonical repair/backfill/reconciliation; local seed | Production cutover, 7-day and 90-day backfill, production reconciliation, query-plan checks, performance benchmarks, and operational telemetry | +| 3. Spend Alert evaluation | Implemented | Fixed anomaly policy, threshold crossing logic, first-enable/re-enable evaluation, web post-spend scheduling, hourly sweep, events, notification delivery creation, and episode dedupe | Production smoke tests and KiloClaw Worker post-renewal dispatch if lower latency than hourly sweep is required | +| 4. Cost Suggestion evaluation | Implemented | Default-on config, eligibility heuristics, evidence windows, active identity, CTA selection, dismissal, and events | Product tuning from production evidence | +| 5. Notifications and banners | Implemented | Recipient snapshots, retryable email deliveries, dispatch-time access checks, Spend Alert email template, dashboard banners, suggestion cards, and deep links | Live email provider smoke test | +| 6. Cost Insights UI | Implemented | Personal/org dashboard, `/config`, activity, UI-only Ask Kilo, tRPC reads/mutations, actor labels, sidebar attention, admin read-only settings, and route cleanup | Browser smoke after deployment | +| 7. Retention and audit cleanup | Implemented | Daily deletion of 90-day events and child delivery rows while compact owner state remains | Production cron smoke | -Prerequisite: complete the spend-writer audit from `.plans/cost-insights-data-layer.md` so every production `microdollars_used` mutation has an included/excluded classification. +## Implementation areas -| Slice | Goal | Primary outcomes | +| Area | Current state | Remaining work | |---|---|---| -| 1. Schema and policy primitives | Establish durable storage and pure policy contracts | Alert and suggestion config/state, owner-hour total, driver-bucket, coverage, degraded-interval, and event tables; shared policy helpers; defaults and validation | -| 2. Spend evidence data layer | Record and expose spend evidence across every Credit-spend path | Atomic Variable/Scheduled capture, dense hourly reads, exact rolling-24h reads, top drivers, 7-day enablement repair, 90-day bootstrap, reconciliation | -| 3. Spend Alert evaluation | Detect anomalies and threshold crossings | Async post-spend evaluation, hourly sweep, event creation, exact threshold semantics, episode dedupe | -| 4. Cost Suggestion evaluation | Create advisory cost-efficiency recommendations | Eligibility/evidence evaluation, active suggestion state, dismissal identity, suggestion events, CTA destinations | -| 5. Notifications and banners | Surface alerts and suggestions without request-side side effects | Email dispatch with per-recipient delivery rows, owner-scoped in-app banner, active suggestion cards, Cost Insights deep links | -| 6. Cost Insights UI | Let owners inspect evidence, configure features, and review outcomes | Dashboard, settings, event history, org member driver links, suggestion actions, sidebar attention state | -| 7. Retention and audit cleanup | Keep event history bounded while preserving rollups and dedupe state | Daily event deletion after 90 days, notification row cleanup, owner state remains compact, rollups remain indefinite | - -## Implementation Areas - -| Area | Expected change | -|---|---| -| `packages/db/src/schema.ts` | Add Cost Insights config, state, rollup, coverage, degraded-interval, suggestion, notification, and event tables. | -| `packages/db/src/cost-insights-rollups.ts` | Add transaction-bound capture primitive shared by web and Worker spend paths. | -| `packages/db/src/migrations/` | Generate migration from schema with `pnpm drizzle generate`. | -| `apps/web/src/lib/ai-gateway/` | Classify Variable Credit spend and atomically update owner-hour rollups without changing request admission. | -| `apps/web/src/lib/organizations/` | Consolidate organization spend mutation and defer existing low-balance email scheduling until commit. | -| `apps/web/src/lib/exa-usage.ts` | Capture charged Exa requests as Variable Credit spend under source `other` and product `exa`. | -| `apps/web/src/lib/kiloclaw/` | Classify pure-credit hosting enrollment as Scheduled Credit spend and update rollups. | -| `services/kiloclaw-billing/` | Capture pure-credit KiloClaw renewals inside existing Worker billing transactions. | -| `apps/web/src/lib/coding-plans/` | Classify plan purchases and renewals as Scheduled Credit spend when applicable. | -| `apps/web/src/lib/cost-insights/` | Add spend reads, coverage, backfill/repair, alert evaluation, Cost Suggestion evaluation, and event workflows. | -| `apps/web/src/routers/` | Add owner-scoped Cost Insights tRPC procedures. | -| `apps/web/src/app/(*)` | Add personal and organization Cost Insights routes. | -| `apps/web/src/components/` | Add dashboard, settings, banners, suggestions, and sidebar attention UI following existing app patterns. | -| `apps/web/src/emails/` | Add Spend Alert and Cost Suggestion emails with appropriate deep links. | - -## Required Tests - -- Alert and suggestion config defaulting, validation, independence, authorization, and organization billing-manager access. -- Closed spend-writer audit covering every production `microdollars_used` mutation. -- Hourly rollup writes for AI Gateway and charged Exa Variable Credit spend plus KiloClaw and Coding Plan Scheduled Credit spend. -- All-owner owner-hour total and driver-bucket writes plus enablement baseline reuse. -- Spend-write rollback when required owner-hour total or driver-bucket capture cannot commit. -- Covered zero-spend hours versus uncovered or degraded unknown hours. -- Prior-7-day enablement backfill/repair and reconciled 90-day completeness before 30d/90d evidence is treated as complete. -- Exact rolling `[asOf - 24h, asOf)` spend reads without UTC-bucket approximation or boundary double counting. -- Fixed anomaly formula (`max(3 * baseline, 10 USD floor)`), 25 USD starter floor, 7-day p95 baseline, and once-per-hour dedupe. -- Single spend-threshold crossing dedupe across exact rolling 24-hour windows. -- Cost Suggestion eligibility, default enablement, independent disablement, evidence windows, dismissal identity, CTA destination, and non-guarantee copy. -- Alert-only regression coverage for AI Gateway, Exa, KiloClaw, Coding Plan, and auto-top-up paths. -- Regression coverage that Spend Alerts and Cost Suggestions never reject paid requests or alter spend/subscription state. -- Org event evidence includes member spend drivers without exposing unauthorized org data. -- Sidebar attention state for unreviewed alert; suggestions alone do not use alert attention semantics. -- Email links route to the correct alert review or suggestion context. -- Per-recipient notification retry without duplicate owner-scoped events. -- Recipient access revalidation that skips org recipients who lost manager access before send. -- Current-manager banner, review, suggestion CTA, and dismissal visibility for owners and billing managers. - -## Verification - -- Run targeted tests for changed web, database, billing, usage, and Cost Insights areas. -- Run targeted type checking or `scripts/typecheck-all.sh --changes-only`; avoid full monorepo typecheck unless broad changes require it. -- Run `pnpm format` before commit. +| `packages/db/src/schema.ts` | Spend evidence, config, owner state, active suggestion, event, and notification delivery tables are implemented | Production migration deployment | +| `packages/db/src/cost-insights-rollups.ts` | Transaction-bound capture is implemented for web and Worker callers | Add telemetry only if it belongs at this boundary; generic reads remain in the web repository | +| `packages/db/src/migrations/` | Generated migrations `0173_workable_carlie_cooper.sql` and `0174_young_molecule_man.sql` contain Spend evidence and alert/suggestion/event storage | Never edit generated migration artifacts by hand | +| `apps/web/src/lib/ai-gateway/` | Positive personal and organization Variable Credit spend captures atomically and schedules async evaluation after commit | Production smoke | +| `apps/web/src/lib/organizations/` | Organization mutation is transaction-aware; Cost Insights reuses owner/billing-manager authorization and recipient checks | Production smoke | +| `apps/web/src/lib/exa-usage.ts` | Charged positive Exa usage captures atomically as `other`/`exa` and schedules async evaluation after commit | Finish production partition-index rollout | +| `apps/web/src/lib/kiloclaw/` | Pure-credit enrollment captures Scheduled Credit spend and schedules async evaluation after commit | Production smoke | +| `services/kiloclaw-billing/` | Pure-credit renewal captures Scheduled Credit spend with request-scoped DB use | Hourly sweep evaluates renewal spend; add direct side-effect dispatch only if production latency requires it | +| `apps/web/src/lib/coding-plans/` | Activation and renewal capture Scheduled Credit spend and schedule async evaluation after commit | Production smoke | +| `apps/web/src/lib/cost-insights/` | Spend reads, config/state/event repositories, alert policy/evaluation, suggestion evaluation, notification workflows, jobs, retention, and presentation mapping exist | Production smoke and tuning | +| `apps/web/src/routers/` | Personal and organization Cost Insights procedures exist for dashboard, settings, event history, acknowledgment, suggestion dismissal, and attention state | Authorization regression expansion | +| `apps/web/src/app/(app)` | Personal and organization routes render live dashboard, activity, UI-only Ask Kilo, and `/config`; old `/settings` paths redirect | Browser smoke | +| `apps/web/src/components/cost-insights/` | Dashboard, UI-only Ask Kilo, settings, activity, banners, suggestions, loading/error states, and actor-label display are wired to live data | Browser smoke | +| `apps/web/src/emails/` | Spend Alert email exists with owner-correct links | Live email provider smoke | +| `apps/web/src/app/api/cron/` and `apps/web/vercel.json` | Hourly evaluation sweep and daily 90-day retention cleanup are registered | Production cron smoke | + +## Required tests + +| Test area | Status | Remaining coverage | +|---|---|---| +| Spend-writer inventory | Done | Keep the repository guard current when new balance mutations are added | +| Atomic capture and rollback | Done for current producers | Add real-rollup integration coverage for Coding Plan and KiloClaw paths that currently mock capture | +| Owner isolation, UTC buckets, covered zero, unknown/degraded history | Done at repository/data-layer level | Add more API authorization and organization read-scope tests | +| Exact rolling `[asOf - 24h, asOf)` | Done | Benchmark high-volume boundary fragments before per-spend threshold evaluation | +| Backfill, repair, reconciliation | Partial | Add production canaries and broader degraded-interval lifecycle coverage | +| Preset evidence ranges | Partial | Add exact 24h, 7d, 30d, and 90d bucket-count tests plus top-driver tie/category tests | +| Config and authorization | Partial | Threshold validation has pure coverage; add router-level owner/billing-manager/member/admin tests | +| Spend Anomaly Alerts | Partial | Policy helper coverage exists; add repository-backed dedupe, acknowledgment, and first-enable tests | +| Spend Threshold Alerts | Partial | Add exact crossing/recovery/recrossing, adjustment, disablement, and first-enable tests | +| Cost Suggestions | Partial | Add repository-backed default enablement, materially-new identity, CTA, and dismissal tests | +| Non-enforcement | Partial | Targeted spend-writer tests still pass; add end-to-end proof that alerts never reject spend or change billing state | +| Events, notifications, banners, and attention | Partial | Add event snapshot, delivery retry/revalidation, and sidebar attention tests | +| Retention | Partial | Add retention job coverage for event and child delivery deletion without owner-state reset | + +## Next implementation order + +1. Deploy migrations and complete data-layer production rollout gates from `.plans/cost-insights-data-layer.md`. +2. Run production EXPLAINs, capture latency and lock benchmarks, and live cron/email smoke tests. +3. Complete contiguous 7-day then 90-day backfill, canary reconciliation, and deployment-boundary reconciliation. +4. Add router/repository integration tests for organization authorization, alert episode dedupe, notification retry/revalidation, and retention. +5. Decide whether KiloClaw Worker renewals need direct post-commit side-effect dispatch or whether hourly sweep latency is acceptable. + +## Verification completed + +Implementation commit `f060ef557` was locally validated with targeted web, database, and KiloClaw Worker tests, targeted typechecks/lint, `pnpm format`, `git diff --check`, and empty-database migration bootstrap. Full monorepo typecheck was skipped under repository guidance. + +This branch was locally validated with: + +- `pnpm --filter @kilocode/db typecheck` +- `pnpm --filter web typecheck` +- `pnpm --filter web lint` +- `pnpm --filter @kilocode/db lint` +- `pnpm --filter web test -- src/lib/cost-insights/policy.test.ts src/lib/exa-usage.test.ts src/lib/coding-plans/index.test.ts src/lib/coding-plans/billing-lifecycle-cron.test.ts src/lib/usageDeduction.test.ts` +- `pnpm format` +- `git diff --check` +- Disposable database migration smoke with `POSTGRES_URL=postgres://postgres:postgres@localhost:5432/ pnpm drizzle migrate` + +`pnpm test:db` started healthy Postgres but `drizzle-kit migrate` returned an unhelpful `undefined` error because the local long-lived test database already had a migration row at the new index from prior local state. A disposable database migration smoke passed with the generated migration set. This local migration-table issue is not evidence of SQL invalidity. + +The local seed was run twice and reconciled across 2,160 hourly buckets with zero mismatches and zero coverage holes. Canonical totals matched rollups for personal and organization fixture owners. This validates local behavior only; it is not evidence of production rollout. diff --git a/.specs/cost-insights.md b/.specs/cost-insights.md index d9da8e620c..3fb3fbc0f7 100644 --- a/.specs/cost-insights.md +++ b/.specs/cost-insights.md @@ -14,7 +14,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S ## Definitions -- **Cost Insights**: Dedicated account surface for viewing spend evidence and configuring Spend Alerts. +- **Cost Insights**: Dedicated Usage-adjacent surface for viewing spend evidence and configuring Spend Alerts. - **Spend Alerts**: Owner-scoped alerting capability for unusual or excessive Credit spend. - **Spend owner**: Personal user or organization whose credit balance is charged for Credit spend. - **Credit spend**: Existing Kilo billing concept for any operation that increments `microdollars_used`. @@ -67,7 +67,7 @@ Cost Insights does not replace low-balance alerts, auto-top-up setup, existing o 2. Personal Cost Insights settings MUST be served at `/cost-insights/config`. 3. Organization Cost Insights dashboard MUST be served at `/organizations/[id]/cost-insights`. 4. Organization Cost Insights settings MUST be served at `/organizations/[id]/cost-insights/config`. -5. Cost Insights MUST appear in the Account section of personal and organization sidebars. +5. Cost Insights MUST appear directly below Usage in personal and organization sidebars. 6. Cost Insights sidebar item MUST show attention state when owner has an unreviewed Spend Alert. 7. Cost Insights routes MUST NOT require a feature flag in v1. diff --git a/CONTEXT.md b/CONTEXT.md index 3e9adfe412..9803e7e745 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -37,7 +37,7 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr | **Email Delivery** | Attempt to render and send one Security Agent Notification through Mailgun | Referring to provider side effect, retry, or acceptance | Notification event | | **Security Finding Activity Event** | Immutable record of one material user, system-policy, or source-driven action or outcome that changes or explains a Security Finding | Referring to evidence included in a Security Agent Audit Report | Page view, unchanged sync observation, queue claim, heartbeat | | **Security Agent Audit Report** | Owner-scoped, period-bounded audit view of Security Finding Activity Events grouped by Security Finding | Referring to the interactive audit report | Generic audit-log export, activity dump | -| **Cost Insights** | Dedicated Account-section surface for viewing spend evidence, configuring Spend Alerts, and acting on Cost Suggestions | Naming the product surface, dashboard, settings, routes, or sidebar item | Spend Protection, Cost Controls | +| **Cost Insights** | Dedicated Usage-adjacent surface for viewing spend evidence, configuring Spend Alerts, and acting on Cost Suggestions | Naming the product surface, dashboard, settings, routes, or sidebar item | Spend Protection, Cost Controls | | **Spend Alerts** | Owner-scoped alerting capability for unusual or excessive Credit spend | Referring to alert evaluation, emails, banners, settings, or notification policy | Spend Protection, hard limit, spend blocker | | **Cost Suggestion** | Optional owner-scoped recommendation based on observed Credit spend that may improve cost efficiency through an eligible Coding Plan or Kilo Pass | Referring to recommendation evaluation, dashboard cards, emails, CTA destinations, dismissal, or settings | Alert, warning, guaranteed savings, automatic optimization | | **Suggestion dismissal** | Authorized owner action that hides one Cost Suggestion without changing billing or future suggestion eligibility | Referring to dismissing a recommendation | Alert acknowledgment, unsubscribe, disable suggestions | @@ -109,9 +109,9 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr - Alert **Cost Insight Events** snapshot top 5 spend drivers at event creation time. - Cost Insight Events store direct evaluated settings in snapshots and do not require config version tracking in v1. - Spend Alert config events store changed fields plus resulting key settings, not full config snapshots. -- **Cost Insights** is the dedicated Account-section surface for Spend Alerts: `/cost-insights` and `/organizations/[id]/cost-insights` are dashboard routes; `/cost-insights/config` and `/organizations/[id]/cost-insights/config` are settings routes. +- **Cost Insights** is the dedicated Usage-adjacent surface for Spend Alerts: `/cost-insights` and `/organizations/[id]/cost-insights` are dashboard routes; `/cost-insights/config` and `/organizations/[id]/cost-insights/config` are settings routes. - Cost Insights dashboard shows current alert state, review actions, and spend evidence. Cost Insights settings owns Spend Alerts policy. -- Account sidebar Cost Insights item shows attention state for unreviewed Spend Alert. +- Cost Insights appears directly below Usage in the personal and organization sidebars and shows attention state for unreviewed Spend Alert. - Organization Cost Insights identifies member spend drivers and links to existing organization member daily limit controls; v1 does not add per-member Spend Alert policy. - Organization Cost Insights dashboard and settings are visible only to organization owners and billing managers. - Organization members who cannot view Cost Insights are told to contact an organization owner or billing manager. diff --git a/apps/storybook/stories/cost-insights/Settings.stories.tsx b/apps/storybook/stories/cost-insights/Settings.stories.tsx index 2da18268e9..8cb1c8ba97 100644 --- a/apps/storybook/stories/cost-insights/Settings.stories.tsx +++ b/apps/storybook/stories/cost-insights/Settings.stories.tsx @@ -17,7 +17,7 @@ type Story = StoryObj; function renderSettings(data: CostInsightsSettingsData) { return ( - + ); diff --git a/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx b/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx index 3c8afb2938..3a1a063dea 100644 --- a/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx +++ b/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx @@ -31,6 +31,7 @@ import { } from 'lucide-react'; import { usePathname } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; import OrganizationSwitcher from './OrganizationSwitcher'; import { useRoleTesting } from '@/contexts/RoleTestingContext'; import HeaderLogo from '@/components/HeaderLogo'; @@ -40,6 +41,7 @@ import SidebarMenuList from './SidebarMenuList'; import SidebarUserFooter from './SidebarUserFooter'; import { ENABLE_DEPLOY_FEATURE } from '@/lib/constants'; import { useFeatureFlagEnabled } from 'posthog-js/react'; +import { useTRPC } from '@/lib/trpc/utils'; type OrganizationAppSidebarProps = React.ComponentProps & { organizationId: string; @@ -49,6 +51,7 @@ export default function OrganizationAppSidebar({ organizationId, ...props }: OrganizationAppSidebarProps) { + const trpc = useTRPC(); const { data: user, isLoading } = useUser(); const pathname = usePathname(); const { assumedRole, setAssumedRole, setOriginalRole } = useRoleTesting(); @@ -101,12 +104,19 @@ export default function OrganizationAppSidebar({ }, [actualRole, user?.is_admin, setOriginalRole, setAssumedRole]); const hasOwnerLevelAccess = currentRole === 'owner' || currentRole === 'billing_manager'; + const { data: costInsightsAttention } = useQuery({ + ...trpc.organizations.costInsights.getAttentionState.queryOptions({ organizationId }), + enabled: hasOwnerLevelAccess || Boolean(user?.is_admin), + staleTime: 60_000, + refetchInterval: 60_000, + }); // Dashboard group const dashboardItems: Array<{ title: string; icon: React.ElementType; url: string; + badge?: string; className?: string; }> = [ ...(showWelcome @@ -134,6 +144,7 @@ export default function OrganizationAppSidebar({ title: 'Cost Insights', icon: ChartLine, url: `/organizations/${organizationId}/cost-insights`, + badge: costInsightsAttention?.attention === 'alert' ? 'Review' : undefined, }, ] : []), diff --git a/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx b/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx index 9d96925668..c066809792 100644 --- a/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx +++ b/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx @@ -59,6 +59,12 @@ export default function PersonalAppSidebar(props: React.ComponentProps = [ { @@ -92,6 +99,7 @@ export default function PersonalAppSidebar(props: React.ComponentProps = [ { diff --git a/apps/web/src/app/(app)/cost-insights/activity/page.tsx b/apps/web/src/app/(app)/cost-insights/activity/page.tsx index c00bdd8ee4..82257aa9d8 100644 --- a/apps/web/src/app/(app)/cost-insights/activity/page.tsx +++ b/apps/web/src/app/(app)/cost-insights/activity/page.tsx @@ -1,5 +1,5 @@ -import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; +import { CostInsightsActivityClient } from '@/components/cost-insights/CostInsightsActivityClient'; export default function CostInsightsActivityPage() { - return ; + return ; } diff --git a/apps/web/src/app/(app)/cost-insights/ask-kilo/page.tsx b/apps/web/src/app/(app)/cost-insights/ask-kilo/page.tsx index d8481c6ed2..98df134d2b 100644 --- a/apps/web/src/app/(app)/cost-insights/ask-kilo/page.tsx +++ b/apps/web/src/app/(app)/cost-insights/ask-kilo/page.tsx @@ -1,5 +1,16 @@ -import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; +import { CostInsightsAskKiloView } from '@/components/cost-insights'; -export default function CostInsightsAskKiloPage() { - return ; +type CostInsightsAskKiloPageProps = { + searchParams?: Promise<{ question?: string | string[] }>; +}; + +export default async function CostInsightsAskKiloPage({ + searchParams, +}: CostInsightsAskKiloPageProps) { + const resolvedSearchParams = await searchParams; + const question = Array.isArray(resolvedSearchParams?.question) + ? resolvedSearchParams.question[0] + : resolvedSearchParams?.question; + + return ; } diff --git a/apps/web/src/app/(app)/cost-insights/config/page.tsx b/apps/web/src/app/(app)/cost-insights/config/page.tsx new file mode 100644 index 0000000000..a40a2bd930 --- /dev/null +++ b/apps/web/src/app/(app)/cost-insights/config/page.tsx @@ -0,0 +1,5 @@ +import { CostInsightsSettingsClient } from '@/components/cost-insights/CostInsightsSettingsClient'; + +export default function CostInsightsConfigPage() { + return ; +} diff --git a/apps/web/src/app/(app)/cost-insights/page.tsx b/apps/web/src/app/(app)/cost-insights/page.tsx index 5c0b567c4f..1aa9a8b36b 100644 --- a/apps/web/src/app/(app)/cost-insights/page.tsx +++ b/apps/web/src/app/(app)/cost-insights/page.tsx @@ -1,5 +1,5 @@ -import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; +import { CostInsightsOverviewClient } from '@/components/cost-insights/CostInsightsOverviewClient'; export default function CostInsightsPage() { - return ; + return ; } diff --git a/apps/web/src/app/(app)/cost-insights/settings/page.tsx b/apps/web/src/app/(app)/cost-insights/settings/page.tsx index 80341752ec..990e9b9c21 100644 --- a/apps/web/src/app/(app)/cost-insights/settings/page.tsx +++ b/apps/web/src/app/(app)/cost-insights/settings/page.tsx @@ -1,5 +1,5 @@ -import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; +import { redirect } from 'next/navigation'; export default function CostInsightsSettingsPage() { - return ; + redirect('/cost-insights/config'); } diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/activity/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/activity/page.tsx index 03c32071d9..fa821ae240 100644 --- a/apps/web/src/app/(app)/organizations/[id]/cost-insights/activity/page.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/activity/page.tsx @@ -1,5 +1,12 @@ -import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; +import { CostInsightsActivityClient } from '@/components/cost-insights/CostInsightsActivityClient'; -export default function OrganizationCostInsightsActivityPage() { - return ; +type OrganizationCostInsightsActivityPageProps = { + params: Promise<{ id: string }>; +}; + +export default async function OrganizationCostInsightsActivityPage({ + params, +}: OrganizationCostInsightsActivityPageProps) { + const { id } = await params; + return ; } diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/ask-kilo/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/ask-kilo/page.tsx index c907d94d10..7200537f75 100644 --- a/apps/web/src/app/(app)/organizations/[id]/cost-insights/ask-kilo/page.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/ask-kilo/page.tsx @@ -1,5 +1,17 @@ -import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; +import { CostInsightsAskKiloView } from '@/components/cost-insights'; -export default function OrganizationCostInsightsAskKiloPage() { - return ; +type OrganizationCostInsightsAskKiloPageProps = { + params: Promise<{ id: string }>; + searchParams?: Promise<{ question?: string | string[] }>; +}; + +export default async function OrganizationCostInsightsAskKiloPage({ + searchParams, +}: OrganizationCostInsightsAskKiloPageProps) { + const resolvedSearchParams = await searchParams; + const question = Array.isArray(resolvedSearchParams?.question) + ? resolvedSearchParams.question[0] + : resolvedSearchParams?.question; + + return ; } diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/config/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/config/page.tsx new file mode 100644 index 0000000000..6630c5a471 --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/config/page.tsx @@ -0,0 +1,12 @@ +import { CostInsightsSettingsClient } from '@/components/cost-insights/CostInsightsSettingsClient'; + +type OrganizationCostInsightsConfigPageProps = { + params: Promise<{ id: string }>; +}; + +export default async function OrganizationCostInsightsConfigPage({ + params, +}: OrganizationCostInsightsConfigPageProps) { + const { id } = await params; + return ; +} diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/page.tsx index 811e0008d4..e93e610120 100644 --- a/apps/web/src/app/(app)/organizations/[id]/cost-insights/page.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/page.tsx @@ -1,5 +1,17 @@ -import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; +import { CostInsightsOverviewClient } from '@/components/cost-insights/CostInsightsOverviewClient'; -export default function OrganizationCostInsightsPage() { - return ; +type OrganizationCostInsightsPageProps = { + params: Promise<{ id: string }>; +}; + +export default async function OrganizationCostInsightsPage({ + params, +}: OrganizationCostInsightsPageProps) { + const { id } = await params; + return ( + + ); } diff --git a/apps/web/src/app/(app)/organizations/[id]/cost-insights/settings/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cost-insights/settings/page.tsx index 3c3347466f..671c43c66c 100644 --- a/apps/web/src/app/(app)/organizations/[id]/cost-insights/settings/page.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/cost-insights/settings/page.tsx @@ -1,5 +1,12 @@ -import { CostInsightsRoutePlaceholder } from '@/components/cost-insights/CostInsightsRoutePlaceholder'; +import { redirect } from 'next/navigation'; -export default function OrganizationCostInsightsSettingsPage() { - return ; +type OrganizationCostInsightsSettingsPageProps = { + params: Promise<{ id: string }>; +}; + +export default async function OrganizationCostInsightsSettingsPage({ + params, +}: OrganizationCostInsightsSettingsPageProps) { + const { id } = await params; + redirect(`/organizations/${id}/cost-insights/config`); } diff --git a/apps/web/src/app/api/cron/cost-insights-hourly/route.ts b/apps/web/src/app/api/cron/cost-insights-hourly/route.ts new file mode 100644 index 0000000000..aeea7bbd6b --- /dev/null +++ b/apps/web/src/app/api/cron/cost-insights-hourly/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server'; + +import { db } from '@/lib/drizzle'; +import { CRON_SECRET } from '@/lib/config.server'; +import { runCostInsightHourlySweep } from '@/lib/cost-insights/jobs'; +import { sentryLogger } from '@/lib/utils.server'; + +if (!CRON_SECRET) { + throw new Error('CRON_SECRET is not configured in environment variables'); +} + +export async function GET(request: Request) { + const authHeader = request.headers.get('authorization'); + const expectedAuth = `Bearer ${CRON_SECRET}`; + if (authHeader !== expectedAuth) { + sentryLogger( + 'cron', + 'warning' + )( + 'SECURITY: Invalid cost-insights-hourly CRON authorization attempt: ' + + (authHeader ? 'Invalid authorization header' : 'Missing authorization header') + ); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const summary = await runCostInsightHourlySweep(db); + + return NextResponse.json( + { + success: true, + summary, + timestamp: new Date().toISOString(), + }, + { status: 200 } + ); +} diff --git a/apps/web/src/app/api/cron/cost-insights-retention/route.ts b/apps/web/src/app/api/cron/cost-insights-retention/route.ts new file mode 100644 index 0000000000..a9d2fecef7 --- /dev/null +++ b/apps/web/src/app/api/cron/cost-insights-retention/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server'; + +import { db } from '@/lib/drizzle'; +import { CRON_SECRET } from '@/lib/config.server'; +import { runCostInsightEventRetentionCleanup } from '@/lib/cost-insights/jobs'; +import { sentryLogger } from '@/lib/utils.server'; + +if (!CRON_SECRET) { + throw new Error('CRON_SECRET is not configured in environment variables'); +} + +export async function GET(request: Request) { + const authHeader = request.headers.get('authorization'); + const expectedAuth = `Bearer ${CRON_SECRET}`; + if (authHeader !== expectedAuth) { + sentryLogger( + 'cron', + 'warning' + )( + 'SECURITY: Invalid cost-insights-retention CRON authorization attempt: ' + + (authHeader ? 'Invalid authorization header' : 'Missing authorization header') + ); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const summary = await runCostInsightEventRetentionCleanup(db); + + return NextResponse.json( + { + success: true, + summary, + timestamp: new Date().toISOString(), + }, + { status: 200 } + ); +} diff --git a/apps/web/src/components/cost-insights/CostInsightsActivityClient.tsx b/apps/web/src/components/cost-insights/CostInsightsActivityClient.tsx new file mode 100644 index 0000000000..1d17b36726 --- /dev/null +++ b/apps/web/src/components/cost-insights/CostInsightsActivityClient.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; + +import { useTRPC } from '@/lib/trpc/utils'; +import { CostInsightsEventHistoryView } from './activity/CostInsightsEventHistoryView'; + +type CostInsightsActivityClientProps = { + organizationId?: string; +}; + +export function CostInsightsActivityClient({ organizationId }: CostInsightsActivityClientProps) { + const trpc = useTRPC(); + const personalEventsQuery = useQuery({ + ...trpc.costInsights.listEvents.queryOptions(), + enabled: !organizationId, + }); + const organizationEventsQuery = useQuery({ + ...trpc.organizations.costInsights.listEvents.queryOptions({ + organizationId: organizationId ?? '', + }), + enabled: Boolean(organizationId), + }); + const eventsQuery = organizationId ? organizationEventsQuery : personalEventsQuery; + + return ( + + ); +} diff --git a/apps/web/src/components/cost-insights/CostInsightsLayout.tsx b/apps/web/src/components/cost-insights/CostInsightsLayout.tsx index bb48dda476..6d88d064dd 100644 --- a/apps/web/src/components/cost-insights/CostInsightsLayout.tsx +++ b/apps/web/src/components/cost-insights/CostInsightsLayout.tsx @@ -16,7 +16,7 @@ const navItems = [ { label: 'Overview', path: '', icon: LayoutDashboard }, { label: 'Ask Kilo', path: '/ask-kilo', icon: MessageCircle }, { label: 'Activity', path: '/activity', icon: Activity }, - { label: 'Alert settings', path: '/settings', icon: Settings2 }, + { label: 'Alert settings', path: '/config', icon: Settings2 }, ]; export function CostInsightsLayout({ basePath, children }: CostInsightsLayoutProps) { diff --git a/apps/web/src/components/cost-insights/CostInsightsOverviewClient.tsx b/apps/web/src/components/cost-insights/CostInsightsOverviewClient.tsx new file mode 100644 index 0000000000..0b50204d9e --- /dev/null +++ b/apps/web/src/components/cost-insights/CostInsightsOverviewClient.tsx @@ -0,0 +1,149 @@ +'use client'; + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; + +import { useTRPC } from '@/lib/trpc/utils'; +import { CostInsightsDashboardView } from './overview/CostInsightsDashboardView'; +import type { DashboardAlert, DashboardAlertAction } from './types'; + +type CostInsightsOverviewClientProps = { + organizationId?: string; + basePath: string; +}; + +export function CostInsightsOverviewClient({ + organizationId, + basePath, +}: CostInsightsOverviewClientProps) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const router = useRouter(); + + const personalDashboardQuery = useQuery({ + ...trpc.costInsights.getDashboard.queryOptions(), + enabled: !organizationId, + }); + const organizationDashboardQuery = useQuery({ + ...trpc.organizations.costInsights.getDashboard.queryOptions({ + organizationId: organizationId ?? '', + }), + enabled: Boolean(organizationId), + }); + const dashboardQuery = organizationId ? organizationDashboardQuery : personalDashboardQuery; + + const invalidateCostInsights = async () => { + if (organizationId) { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: trpc.organizations.costInsights.getDashboard.queryKey({ organizationId }), + }), + queryClient.invalidateQueries({ + queryKey: trpc.organizations.costInsights.listEvents.queryKey({ organizationId }), + }), + queryClient.invalidateQueries({ + queryKey: trpc.organizations.costInsights.getAttentionState.queryKey({ + organizationId, + }), + }), + ]); + return; + } + + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: trpc.costInsights.getDashboard.queryKey(), + }), + queryClient.invalidateQueries({ + queryKey: trpc.costInsights.listEvents.queryKey(), + }), + queryClient.invalidateQueries({ + queryKey: trpc.costInsights.getAttentionState.queryKey(), + }), + ]); + }; + + const personalAcknowledgeMutation = useMutation( + trpc.costInsights.acknowledgeAlert.mutationOptions({ + onSuccess: () => { + toast.success('Alert marked reviewed'); + void invalidateCostInsights(); + }, + onError: error => toast.error(error.message || 'Could not mark alert reviewed'), + }) + ); + const organizationAcknowledgeMutation = useMutation( + trpc.organizations.costInsights.acknowledgeAlert.mutationOptions({ + onSuccess: () => { + toast.success('Alert marked reviewed'); + void invalidateCostInsights(); + }, + onError: error => toast.error(error.message || 'Could not mark alert reviewed'), + }) + ); + const personalDismissMutation = useMutation( + trpc.costInsights.dismissSuggestion.mutationOptions({ + onSuccess: () => { + toast.success('Suggestion dismissed'); + void invalidateCostInsights(); + }, + onError: error => toast.error(error.message || 'Could not dismiss suggestion'), + }) + ); + const organizationDismissMutation = useMutation( + trpc.organizations.costInsights.dismissSuggestion.mutationOptions({ + onSuccess: () => { + toast.success('Suggestion dismissed'); + void invalidateCostInsights(); + }, + onError: error => toast.error(error.message || 'Could not dismiss suggestion'), + }) + ); + + const handleAlertAction = (alert: DashboardAlert, action: DashboardAlertAction) => { + if (action === 'acknowledge') { + if (organizationId) { + organizationAcknowledgeMutation.mutate({ organizationId, alertKind: alert.type }); + return; + } + personalAcknowledgeMutation.mutate({ alertKind: alert.type }); + return; + } + + if (action === 'view_spend') { + document.getElementById('spend-summary-title')?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + return; + } + + router.push(`${basePath}/config`); + }; + + const handleSuggestionDismiss = (suggestionId: string) => { + if (organizationId) { + organizationDismissMutation.mutate({ organizationId, suggestionId }); + return; + } + personalDismissMutation.mutate({ suggestionId }); + }; + + const handleAskKilo = (question: string) => { + const searchParams = new URLSearchParams({ question }); + router.push(`${basePath}/ask-kilo?${searchParams.toString()}`); + }; + + return ( + router.push(`${basePath}/config`)} + onAlertAction={handleAlertAction} + onSuggestionDismiss={handleSuggestionDismiss} + onAskKilo={handleAskKilo} + /> + ); +} diff --git a/apps/web/src/components/cost-insights/CostInsightsRoutePlaceholder.tsx b/apps/web/src/components/cost-insights/CostInsightsRoutePlaceholder.tsx deleted file mode 100644 index a272e38367..0000000000 --- a/apps/web/src/components/cost-insights/CostInsightsRoutePlaceholder.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; - -export function CostInsightsRoutePlaceholder({ - section, -}: { - section: 'Overview' | 'Ask Kilo' | 'Activity' | 'Alert settings'; -}) { - return ( - - - {section} - - Cost Insights data will appear here when spend data is connected. - - - - ); -} diff --git a/apps/web/src/components/cost-insights/CostInsightsSettingsClient.tsx b/apps/web/src/components/cost-insights/CostInsightsSettingsClient.tsx new file mode 100644 index 0000000000..68acbf1571 --- /dev/null +++ b/apps/web/src/components/cost-insights/CostInsightsSettingsClient.tsx @@ -0,0 +1,166 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; + +import { Skeleton } from '@/components/ui/skeleton'; +import { useTRPC } from '@/lib/trpc/utils'; +import { CostInsightsLoadError } from './shared/CostInsightsLoadError'; +import { CostInsightsSettingsView } from './settings/CostInsightsSettingsView'; +import type { CostInsightsSettingsData, CostInsightsSettingsPatch } from './types'; + +type SettingsFormState = Pick< + CostInsightsSettingsData, + 'enabled' | 'suggestionsEnabled' | 'thresholdUsd' +>; + +type CostInsightsSettingsClientProps = { + organizationId?: string; +}; + +const THRESHOLD_USD_PATTERN = /^(?:0|[1-9]\d*)(?:\.(\d{1,2}))?$/; + +function validateThresholdUsd(value: string): string | undefined { + const trimmed = value.trim(); + if (trimmed === '') return undefined; + if (!THRESHOLD_USD_PATTERN.test(trimmed)) { + return 'Enter a positive USD amount with up to 2 decimal places.'; + } + const [wholePart, centsPart = ''] = trimmed.split('.'); + const dollars = Number.parseInt(wholePart, 10); + const cents = Number.parseInt(centsPart.padEnd(2, '0') || '0', 10); + const totalCents = dollars * 100 + cents; + if (!Number.isSafeInteger(totalCents) || totalCents <= 0) { + return 'Enter an amount greater than $0.00.'; + } + return undefined; +} + +export function CostInsightsSettingsClient({ organizationId }: CostInsightsSettingsClientProps) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [form, setForm] = useState(null); + + const personalSettingsQuery = useQuery({ + ...trpc.costInsights.getSettings.queryOptions(), + enabled: !organizationId, + }); + const organizationSettingsQuery = useQuery({ + ...trpc.organizations.costInsights.getSettings.queryOptions({ + organizationId: organizationId ?? '', + }), + enabled: Boolean(organizationId), + }); + const settingsQuery = organizationId ? organizationSettingsQuery : personalSettingsQuery; + const settings = settingsQuery.data; + + useEffect(() => { + if (!settings) return; + setForm({ + enabled: settings.enabled, + suggestionsEnabled: settings.suggestionsEnabled, + thresholdUsd: settings.thresholdUsd, + }); + }, [settings?.enabled, settings?.suggestionsEnabled, settings?.thresholdUsd]); + + const invalidateCostInsights = async () => { + if (organizationId) { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: trpc.organizations.costInsights.getDashboard.queryKey({ organizationId }), + }), + queryClient.invalidateQueries({ + queryKey: trpc.organizations.costInsights.getSettings.queryKey({ organizationId }), + }), + queryClient.invalidateQueries({ + queryKey: trpc.organizations.costInsights.listEvents.queryKey({ organizationId }), + }), + queryClient.invalidateQueries({ + queryKey: trpc.organizations.costInsights.getAttentionState.queryKey({ + organizationId, + }), + }), + ]); + return; + } + + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: trpc.costInsights.getDashboard.queryKey(), + }), + queryClient.invalidateQueries({ + queryKey: trpc.costInsights.getSettings.queryKey(), + }), + queryClient.invalidateQueries({ + queryKey: trpc.costInsights.listEvents.queryKey(), + }), + queryClient.invalidateQueries({ + queryKey: trpc.costInsights.getAttentionState.queryKey(), + }), + ]); + }; + + const personalUpdateMutation = useMutation( + trpc.costInsights.updateSettings.mutationOptions({ + onSuccess: () => { + toast.success('Cost Insights settings saved'); + void invalidateCostInsights(); + }, + onError: error => toast.error(error.message || 'Could not save Cost Insights settings'), + }) + ); + const organizationUpdateMutation = useMutation( + trpc.organizations.costInsights.updateSettings.mutationOptions({ + onSuccess: () => { + toast.success('Cost Insights settings saved'); + void invalidateCostInsights(); + }, + onError: error => toast.error(error.message || 'Could not save Cost Insights settings'), + }) + ); + + if (settingsQuery.isLoading) return ; + if (settingsQuery.isError || !settings || !form) return ; + + const validation = validateThresholdUsd(form.thresholdUsd); + const dirty = + form.enabled !== settings.enabled || + form.suggestionsEnabled !== settings.suggestionsEnabled || + form.thresholdUsd !== settings.thresholdUsd; + const activeMutation = organizationId ? organizationUpdateMutation : personalUpdateMutation; + const saveState: CostInsightsSettingsData['saveState'] = activeMutation.isPending + ? 'saving' + : activeMutation.isError + ? 'error' + : dirty + ? 'dirty' + : 'saved'; + + const data: CostInsightsSettingsData = { + ...settings, + ...form, + saveState, + validations: validation ? [validation] : undefined, + }; + + const handleChange = (patch: CostInsightsSettingsPatch) => { + setForm(current => (current ? { ...current, ...patch } : current)); + }; + + const handleSave = () => { + if (!dirty || validation || settings.readOnly) return; + const input = { + spendAlertsEnabled: form.enabled, + costSuggestionsEnabled: form.suggestionsEnabled, + spendThresholdUsd: form.thresholdUsd.trim() === '' ? null : form.thresholdUsd.trim(), + }; + if (organizationId) { + organizationUpdateMutation.mutate({ organizationId, ...input }); + return; + } + personalUpdateMutation.mutate(input); + }; + + return ; +} diff --git a/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx b/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx index 60ff5c42a5..c3a014999d 100644 --- a/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx +++ b/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx @@ -2,6 +2,7 @@ import { useState, type FormEvent } from 'react'; import { BarChart3, ChevronDown, ChevronUp, Send } from 'lucide-react'; + import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -16,19 +17,28 @@ const askKiloChartData = [ { date: 'Jun 24', cost: 0.31, color: 'var(--chart-2)' }, ]; +type AskKiloMessage = { + id: string; + question: string; +}; + export function CostInsightsAskKiloView({ initialQuestion = 'Create a graph of my costs for the last week', }: { initialQuestion?: string; }) { + const initialMessage = initialQuestion.trim() || 'Create a graph of my costs for the last week'; const [question, setQuestion] = useState(''); - const [messages, setMessages] = useState([{ id: 'initial', question: initialQuestion }]); + const [messages, setMessages] = useState([ + { id: 'initial', question: initialMessage }, + ]); const [chartExpanded, setChartExpanded] = useState(true); function handleSubmit(event: FormEvent) { event.preventDefault(); const trimmedQuestion = question.trim(); if (!trimmedQuestion) return; + setMessages(currentMessages => [ ...currentMessages, { id: `question-${currentMessages.length}`, question: trimmedQuestion }, @@ -37,7 +47,7 @@ export function CostInsightsAskKiloView({ } return ( -
+
{messages.map(message => (
@@ -64,16 +74,16 @@ export function CostInsightsAskKiloView({ {chartExpanded && (

- Model usage · Cost · Jun 18, 2026 to Jun 24, 2026 + Model usage, Cost, Jun 18, 2026 to Jun 24, 2026

-

Cost by date

+

Cost by date

Daily cost from June 18 to June 24. Peak cost was $1.42 on June 18. No spend occurred June 21 or June 22.
-
+
{[1.6, 1.2, 0.8, 0.4, 0].map(value => ( ${value.toFixed(2)} ))} @@ -102,15 +112,15 @@ export function CostInsightsAskKiloView({ backgroundColor: item.color, }} /> - + {item.date.replace('Jun ', '')}
))}
-
- Jun 18–24 +
+ Jun 18-24
@@ -118,7 +128,7 @@ export function CostInsightsAskKiloView({
-

Here is your daily cost trend for the last 7 days (Jun 18–24):

+

Here is your daily cost trend for the last 7 days (Jun 18-24):

  • Total spend: $2.63 over the week @@ -127,11 +137,11 @@ export function CostInsightsAskKiloView({ Daily average: $0.38
  • - Peak day: Jun 18 at $1.42, 54% of - the week's cost + Peak day: + Jun 18 at $1.42, 54% of the week's cost
  • - Quietest days: Jun 21–22 with no + Quietest days: Jun 21-22 with no Credit spend
  • @@ -149,7 +159,7 @@ export function CostInsightsAskKiloView({ ))}
-
+ @@ -158,7 +168,7 @@ export function CostInsightsAskKiloView({ id="ask-kilo-follow-up" value={question} onChange={event => setQuestion(event.target.value)} - placeholder="Ask a follow-up about your spending..." + placeholder="Ask a follow-up about your spending" className="bg-card h-12! rounded-xl pr-14 shadow-lg" />
@@ -37,9 +41,11 @@ export function DisabledAlertsBanner() { export function ReviewBanner({ alert, primaryAction, + onAction, }: { alert: DashboardAlert; primaryAction: boolean; + onAction?: (action: DashboardAlertAction) => void; }) { const Icon = alert.type === 'threshold' ? AlertTriangle : TrendingUp; return ( @@ -71,7 +77,7 @@ export function ReviewBanner({ )}
- + ); @@ -80,9 +86,11 @@ export function ReviewBanner({ function ReviewActions({ alert, primaryAction, + onAction, }: { alert: DashboardAlert; primaryAction: boolean; + onAction?: (action: DashboardAlertAction) => void; }) { return (
@@ -92,6 +100,7 @@ function ReviewActions({ type="button" variant={index === 0 && primaryAction ? 'default' : 'outline'} className="min-h-control-touch w-full sm:min-h-0" + onClick={() => onAction?.(action)} > {action.includes('disable') ? (
@@ -128,8 +139,9 @@ export function CostInsightsSettingsView({ data }: { data: CostInsightsSettingsD type="text" inputMode="decimal" value={data.thresholdUsd} - readOnly - disabled={data.readOnly} + readOnly={data.readOnly} + disabled={disabled} + onChange={event => onChange?.({ thresholdUsd: event.target.value })} aria-invalid={Boolean(validation)} aria-describedby="threshold-help threshold-error" /> @@ -169,8 +181,11 @@ export function CostInsightsSettingsView({ data }: { data: CostInsightsSettingsD + {canManage && ( + + )} ); @@ -41,10 +51,14 @@ export function DisabledAlertsBanner({ onSetupAlerts }: { onSetupAlerts?: () => export function ReviewBanner({ alert, primaryAction, + actionsDisabled = false, + canManage = true, onAction, }: { alert: DashboardAlert; primaryAction: boolean; + actionsDisabled?: boolean; + canManage?: boolean; onAction?: (action: DashboardAlertAction) => void; }) { const Icon = alert.type === 'threshold' ? AlertTriangle : TrendingUp; @@ -53,7 +67,7 @@ export function ReviewBanner({ className="border-status-warning-border bg-status-warning-surface rounded-xl border p-6" aria-labelledby={`alert-${alert.type}`} > -
+
- + {canManage && ( + + )}
); @@ -86,10 +107,12 @@ export function ReviewBanner({ function ReviewActions({ alert, primaryAction, + actionsDisabled, onAction, }: { alert: DashboardAlert; primaryAction: boolean; + actionsDisabled: boolean; onAction?: (action: DashboardAlertAction) => void; }) { return ( @@ -100,6 +123,8 @@ function ReviewActions({ type="button" variant={index === 0 && primaryAction ? 'default' : 'outline'} className="min-h-control-touch w-full sm:min-h-0" + disabled={actionsDisabled} + aria-busy={actionsDisabled && action === 'acknowledge'} onClick={() => onAction?.(action)} > {action.includes('disable') ? ( @@ -116,9 +141,11 @@ function ReviewActions({ export function SuggestionCard({ suggestion, + canManage = true, onDismiss, }: { suggestion: CostSuggestion; + canManage?: boolean; onDismiss?: () => void; }) { return ( @@ -126,7 +153,7 @@ export function SuggestionCard({ className="border-status-success-border bg-status-success-surface rounded-xl border p-6" aria-labelledby={`suggestion-${suggestion.id}`} > -
+
-
- - -
+ {canManage && ( +
+ + +
+ )}
); diff --git a/apps/web/src/components/cost-insights/overview/EventPreviewCard.tsx b/apps/web/src/components/cost-insights/overview/EventPreviewCard.tsx index 34e95f5e82..3188cdb147 100644 --- a/apps/web/src/components/cost-insights/overview/EventPreviewCard.tsx +++ b/apps/web/src/components/cost-insights/overview/EventPreviewCard.tsx @@ -4,7 +4,13 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { EventList } from '../activity/EventList'; import type { CostInsightEvent } from '../types'; -export function EventPreviewCard({ events }: { events: CostInsightEvent[] }) { +export function EventPreviewCard({ + events, + activityHref, +}: { + events: CostInsightEvent[]; + activityHref?: string; +}) { return ( @@ -12,10 +18,14 @@ export function EventPreviewCard({ events }: { events: CostInsightEvent[] }) { Recent activity Alerts, suggestions, reviews, and settings changes.
- + {activityHref && ( + + )} diff --git a/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx b/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx index 2bbca2660d..54815b0e8d 100644 --- a/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx +++ b/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx @@ -2,11 +2,11 @@ import { useState, type CSSProperties } from 'react'; import { ArrowRight, Clock3 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; -import { money, percentOf } from '../formatting'; +import { money, percentOf, spendBarHeightPercent } from '../formatting'; import { EmptyPanel } from '../shared/EmptyPanel'; import type { CostInsightsDashboardData, SpendRange } from '../types'; @@ -42,28 +42,29 @@ export function SpendEvidenceCard({ data }: { data: CostInsightsDashboardData }) {rangeLabel}. Usage-based and scheduled spend are shown separately.
- { - if (isSpendRange(value)) setSelectedRange(value); - }} +
- - {(['24h', '7d', '30d', '90d'] as SpendRange[]).map(range => ( - - {range} - - ))} - - + {(['24h', '7d', '30d', '90d'] as SpendRange[]).map(option => ( + + ))} +
{evidence.length === 0 ? ( @@ -113,7 +114,7 @@ export function SpendEvidenceCard({ data }: { data: CostInsightsDashboardData })
{evidence.map((point, index) => { const pointTotal = point.variableUsd + point.scheduledUsd; - const totalHeight = Math.max(2, percentOf(pointTotal, maxSpend)); + const totalHeight = spendBarHeightPercent(pointTotal, maxSpend); const scheduledShare = percentOf(point.scheduledUsd, pointTotal); return ( diff --git a/apps/web/src/components/cost-insights/overview/TopDriversCard.tsx b/apps/web/src/components/cost-insights/overview/TopDriversCard.tsx index 51ac35d724..0cc91375f8 100644 --- a/apps/web/src/components/cost-insights/overview/TopDriversCard.tsx +++ b/apps/web/src/components/cost-insights/overview/TopDriversCard.tsx @@ -20,7 +20,7 @@ export function TopDriversCard({ Where spend went - Largest contributors in the selected period. + Largest contributors in the last 24 hours. {drivers.length === 0 ? ( @@ -31,7 +31,7 @@ export function TopDriversCard({ ) : (
    {drivers.slice(0, 5).map(driver => ( -
  1. +
-
- + + void }) { + const handleRetry = onRetry ?? (() => window.location.reload()); return (
- + )} + @@ -190,7 +198,7 @@ export function CostInsightsSettingsView({ type="button" className="min-h-control-touch sm:min-h-0" disabled={ - data.saveState === 'saved' || data.saveState === 'saving' || Boolean(validation) + data.saveState === 'saved' || data.saveState === 'saving' || hasValidationError } aria-busy={data.saveState === 'saving'} onClick={onSave} @@ -210,3 +218,70 @@ export function CostInsightsSettingsView({ ); } + +function ThresholdOption({ + id, + title, + description, + value, + validation, + disabled, + readOnly, + onChange, +}: { + id: string; + title: string; + description: string; + value: string; + validation?: string; + disabled: boolean; + readOnly?: boolean; + onChange: (value: string) => void; +}) { + const inputId = `${id}-input`; + const helpId = `${id}-help`; + const errorId = `${id}-error`; + return ( +
+
+
+

+ {title} +

+

{description}

+
+
+ +
+ + onChange(event.target.value)} + aria-invalid={Boolean(validation)} + aria-describedby={validation ? `${helpId} ${errorId}` : helpId} + /> +
+

+ Leave blank to turn off this threshold. +

+ {validation && ( +

+ {validation} +

+ )} +
+
+
+ ); +} diff --git a/apps/web/src/components/cost-insights/shared/LocalDateTime.tsx b/apps/web/src/components/cost-insights/shared/LocalDateTime.tsx new file mode 100644 index 0000000000..ed8f484f16 --- /dev/null +++ b/apps/web/src/components/cost-insights/shared/LocalDateTime.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { useSyncExternalStore } from 'react'; +import { formatCostInsightDateTime } from '../formatting'; + +const subscribe = () => () => {}; + +export function useViewerTimeZone() { + return useSyncExternalStore( + subscribe, + () => true, + () => false + ) + ? undefined + : 'UTC'; +} + +export function LocalDateTime({ + timestamp, + prefix = '', + className, +}: { + timestamp: string; + prefix?: string; + className?: string; +}) { + const label = formatCostInsightDateTime(timestamp, useViewerTimeZone()); + + return ( + + ); +} diff --git a/apps/web/src/components/cost-insights/types.ts b/apps/web/src/components/cost-insights/types.ts index 410c30aac3..f5a97a83d8 100644 --- a/apps/web/src/components/cost-insights/types.ts +++ b/apps/web/src/components/cost-insights/types.ts @@ -8,7 +8,7 @@ export type CostInsightsOwner = { export type CostInsightsPage = 'dashboard' | 'ask' | 'events' | 'config'; export type CostInsightsAttention = 'none' | 'alert'; -export type SpendRange = '24h' | '7d' | '30d' | '90d'; +export type SpendRange = '1h' | '24h' | '7d' | '30d' | '90d'; export type SpendMetricIcon = 'activity' | 'alert' | 'check' | 'dollar'; export type SpendMetric = { @@ -21,6 +21,7 @@ export type SpendMetric = { export type SpendEvidencePoint = { label: string; + periodStart?: string; variableUsd: number; scheduledUsd: number; anomalyThresholdUsd?: number; @@ -40,20 +41,32 @@ export type SpendDriver = { export type AlertFact = { label: string; value: string }; +export type AlertDriverEvidence = { + title: string; + description: string; + periodStart?: string; + periodEndExclusive?: string; + drivers: SpendDriver[]; + totalSpendUsd: number; + scope: 'current_hour' | 'rolling_24h' | 'rolling_7d' | 'rolling_30d' | 'legacy'; +}; + export type DashboardAlert = | { type: 'anomaly'; title: string; description: string; facts?: AlertFact[]; + driverEvidence?: AlertDriverEvidence; actions: ('acknowledge' | 'view_spend' | 'disable_alerts')[]; } | { - type: 'threshold'; + type: 'threshold' | 'threshold_7d' | 'threshold_30d'; title: string; description: string; facts?: AlertFact[]; - actions: ('acknowledge' | 'adjust_threshold' | 'disable_threshold')[]; + driverEvidence?: AlertDriverEvidence; + actions: ('acknowledge' | 'view_spend' | 'manage_threshold')[]; }; export type DashboardAlertAction = DashboardAlert['actions'][number]; @@ -75,11 +88,11 @@ export type CostInsightsDashboardData = { range: SpendRange; metrics: SpendMetric[]; evidence: SpendEvidencePoint[]; - evidenceByRange?: Partial>; - drivers: SpendDriver[]; + evidenceByRange: Record; + driversByRange: Record; alerts: DashboardAlert[]; suggestions: CostSuggestion[]; - lastEvaluatedLabel: string; + lastEvaluatedAt: string | null; baselineMode: 'starter' | 'available-history' | 'seven-day'; eventPreview: CostInsightEvent[]; memberLimitsHref?: string; @@ -99,25 +112,40 @@ export type CostInsightEvent = { type: CostInsightEventType; title: string; description: string; - timestampLabel: string; + occurredAt: string; actorLabel?: string; amountLabel?: string; - amountClassifier?: 'current hour' | 'rolling 24h' | 'last 7 days'; + amountClassifier?: 'current hour' | 'rolling 24h' | 'rolling 7d' | 'rolling 30d' | 'last 7 days'; topDrivers?: SpendDriver[]; }; export type CostInsightsSettingsData = { owner: CostInsightsOwner; enabled: boolean; + anomalyAlertsEnabled: boolean; suggestionsEnabled: boolean; thresholdUsd: string; + threshold7DayUsd?: string; + threshold30DayUsd: string; saveState: 'saved' | 'dirty' | 'saving' | 'error'; - validations?: string[]; + validations?: { + thresholdUsd?: string; + threshold7DayUsd?: string; + threshold30DayUsd?: string; + }; readOnly?: boolean; }; export type CostInsightsSettingsPatch = Partial< - Pick + Pick< + CostInsightsSettingsData, + | 'enabled' + | 'anomalyAlertsEnabled' + | 'suggestionsEnabled' + | 'thresholdUsd' + | 'threshold7DayUsd' + | 'threshold30DayUsd' + > >; export type SettingsConfirmation = diff --git a/apps/web/src/components/cost-insights/useCostInsightsTracking.ts b/apps/web/src/components/cost-insights/useCostInsightsTracking.ts new file mode 100644 index 0000000000..f635313a9a --- /dev/null +++ b/apps/web/src/components/cost-insights/useCostInsightsTracking.ts @@ -0,0 +1,50 @@ +'use client'; + +import { useCallback } from 'react'; +import { useMutation } from '@tanstack/react-query'; + +import { useTRPC } from '@/lib/trpc/utils'; +import type { + CostInsightsSuggestionCta, + CostInsightsUiInteraction, +} from '@/lib/cost-insights/tracking'; + +export function useCostInsightsTracking(organizationId?: string) { + const trpc = useTRPC(); + const { mutate: trackPersonalInteraction } = useMutation( + trpc.costInsights.trackUiInteraction.mutationOptions() + ); + const { mutate: trackOrganizationInteraction } = useMutation( + trpc.organizations.costInsights.trackUiInteraction.mutationOptions() + ); + const { mutate: trackPersonalSuggestionCta } = useMutation( + trpc.costInsights.trackSuggestionCta.mutationOptions() + ); + const { mutate: trackOrganizationSuggestionCta } = useMutation( + trpc.organizations.costInsights.trackSuggestionCta.mutationOptions() + ); + + const trackUiInteraction = useCallback( + (interaction: CostInsightsUiInteraction) => { + if (organizationId) { + trackOrganizationInteraction({ organizationId, ...interaction }); + return; + } + trackPersonalInteraction(interaction); + }, + [organizationId, trackOrganizationInteraction, trackPersonalInteraction] + ); + + const trackSuggestionCta = useCallback( + (suggestion: CostInsightsSuggestionCta) => { + if (organizationId) { + trackOrganizationSuggestionCta({ organizationId, ...suggestion }); + return; + } + trackPersonalSuggestionCta(suggestion); + }, + [organizationId, trackOrganizationSuggestionCta, trackPersonalSuggestionCta] + ); + + return { trackUiInteraction, trackSuggestionCta }; +} diff --git a/apps/web/src/lib/cost-insights/evaluation.integration.test.ts b/apps/web/src/lib/cost-insights/evaluation.integration.test.ts new file mode 100644 index 0000000000..4ab557f89b --- /dev/null +++ b/apps/web/src/lib/cost-insights/evaluation.integration.test.ts @@ -0,0 +1,325 @@ +import { afterEach, describe, expect, test } from '@jest/globals'; +import { captureCostInsightSpend } from '@kilocode/db/cost-insights-rollups'; +import { + cost_insight_events, + cost_insight_owner_configs, + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, + cost_insight_owner_states, + kilocode_users, + microdollar_usage, +} from '@kilocode/db/schema'; +import { and, eq } from 'drizzle-orm'; + +import { db } from '@/lib/drizzle'; +import { addHours, floorUtcHour } from './policy'; +import { getOwnerRollingDriverEvidenceExact, getOwnerRollingSpendExact } from './spend-repository'; +import { evaluateCostInsightsForOwner } from './evaluation'; + +const testUserIds = new Set(); + +async function createUser(): Promise { + const id = `cost-insights-evaluation-${crypto.randomUUID()}`; + testUserIds.add(id); + await db.insert(kilocode_users).values({ + id, + google_user_email: `${id}@example.com`, + google_user_name: 'Cost Insights Evaluation Test', + google_user_image_url: 'https://example.com/avatar.png', + stripe_customer_id: `cus_${crypto.randomUUID()}`, + }); + return id; +} + +afterEach(async () => { + for (const userId of testUserIds) { + await db.delete(microdollar_usage).where(eq(microdollar_usage.kilo_user_id, userId)); + await db + .delete(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_user_id, userId)); + await db.delete(cost_insight_events).where(eq(cost_insight_events.owned_by_user_id, userId)); + await db + .delete(cost_insight_owner_configs) + .where(eq(cost_insight_owner_configs.owned_by_user_id, userId)); + await db + .delete(cost_insight_owner_hour_driver_buckets) + .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); + await db + .delete(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); + await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); + } + testUserIds.clear(); +}); + +describe('Cost Insights evaluation integration', () => { + test('does not create anomaly events when anomaly alerting is opted out', async () => { + const userId = await createUser(); + const owner = { type: 'user', id: userId } as const; + const asOf = new Date().toISOString(); + const currentHourStart = floorUtcHour(new Date(asOf)); + + await db.insert(cost_insight_owner_configs).values({ + owned_by_user_id: userId, + spend_alerts_enabled: true, + anomaly_alerts_enabled: false, + cost_suggestions_enabled: false, + }); + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt: currentHourStart, + amountMicrodollars: 30_000_000, + category: 'variable', + source: 'ai_gateway', + productKey: 'cli', + featureKey: 'messages', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }); + + const result = await evaluateCostInsightsForOwner(db, owner, { asOf }); + const events = await db + .select() + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, userId)); + + expect(result.anomalyEventCreated).toBe(false); + expect(events).toHaveLength(0); + }); + + test('creates an independent rolling 30-day threshold alert', async () => { + const userId = await createUser(); + const owner = { type: 'user', id: userId } as const; + const asOf = new Date().toISOString(); + const currentHourStart = floorUtcHour(new Date(asOf)); + const spendAt = new Date(Date.parse(asOf) - 1_000).toISOString(); + const usageId = crypto.randomUUID(); + + await db.insert(cost_insight_owner_configs).values({ + owned_by_user_id: userId, + spend_alerts_enabled: true, + anomaly_alerts_enabled: false, + cost_suggestions_enabled: false, + spend_30_day_threshold_microdollars: 20_000_000, + }); + await db.insert(microdollar_usage).values({ + id: usageId, + kilo_user_id: userId, + cost: 30_000_000, + input_tokens: 1, + output_tokens: 1, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: spendAt, + requested_model: 'anthropic/claude-sonnet-4', + model: 'anthropic/claude-sonnet-4', + provider: 'anthropic', + inference_provider: 'anthropic', + }); + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt: currentHourStart, + amountMicrodollars: 30_000_000, + category: 'variable', + source: 'ai_gateway', + productKey: 'cli', + featureKey: 'messages', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }); + + await expect( + getOwnerRollingSpendExact(db, { + owner, + asOf, + windowHours: 720, + fallbackToCanonical: true, + }) + ).resolves.toMatchObject({ totalMicrodollars: 30_000_000, isComplete: true }); + await expect( + getOwnerRollingDriverEvidenceExact(db, { owner, asOf, windowHours: 720 }) + ).resolves.toMatchObject({ totalMicrodollars: 30_000_000 }); + + const result = await evaluateCostInsightsForOwner(db, owner, { asOf }); + const [event] = await db + .select() + .from(cost_insight_events) + .where( + and( + eq(cost_insight_events.owned_by_user_id, userId), + eq(cost_insight_events.alert_kind, 'threshold_30d') + ) + ); + const [state] = await db + .select() + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_user_id, userId)); + + expect(result.threshold30DayEventCreated).toBe(true); + expect(event?.snapshot).toMatchObject({ + thresholdMicrodollars: 20_000_000, + thresholdWindow: 'rolling_30d', + rolling30DayMicrodollars: 30_000_000, + }); + expect(state).toMatchObject({ + rolling_30_day_threshold_crossing_active: true, + active_rolling_30_day_threshold_event_id: event?.id, + rolling_30_day_threshold_reviewed_at: null, + }); + }); + + test('creates an independent rolling 7-day threshold alert', async () => { + const userId = await createUser(); + const owner = { type: 'user', id: userId } as const; + const asOf = new Date().toISOString(); + const currentHourStart = floorUtcHour(new Date(asOf)); + const spendAt = new Date(Date.parse(asOf) - 1_000).toISOString(); + const usageId = crypto.randomUUID(); + + await db.insert(cost_insight_owner_configs).values({ + owned_by_user_id: userId, + spend_alerts_enabled: true, + anomaly_alerts_enabled: false, + cost_suggestions_enabled: false, + spend_7_day_threshold_microdollars: 20_000_000, + }); + await db.insert(microdollar_usage).values({ + id: usageId, + kilo_user_id: userId, + cost: 30_000_000, + input_tokens: 1, + output_tokens: 1, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: spendAt, + requested_model: 'anthropic/claude-sonnet-4', + model: 'anthropic/claude-sonnet-4', + provider: 'anthropic', + inference_provider: 'anthropic', + }); + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt: currentHourStart, + amountMicrodollars: 30_000_000, + category: 'variable', + source: 'ai_gateway', + productKey: 'cli', + featureKey: 'messages', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }); + + await expect( + getOwnerRollingSpendExact(db, { + owner, + asOf, + windowHours: 7 * 24, + fallbackToCanonical: true, + }) + ).resolves.toMatchObject({ totalMicrodollars: 30_000_000, isComplete: true }); + + const result = await evaluateCostInsightsForOwner(db, owner, { asOf }); + const [event] = await db + .select() + .from(cost_insight_events) + .where( + and( + eq(cost_insight_events.owned_by_user_id, userId), + eq(cost_insight_events.alert_kind, 'threshold_7d') + ) + ); + const [state] = await db + .select() + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_user_id, userId)); + + expect(result.threshold7DayEventCreated).toBe(true); + expect(event?.snapshot).toMatchObject({ + thresholdMicrodollars: 20_000_000, + thresholdWindow: 'rolling_7d', + rolling7DayMicrodollars: 30_000_000, + }); + expect(state).toMatchObject({ + rolling_7_day_threshold_crossing_active: true, + active_rolling_7_day_threshold_event_id: event?.id, + rolling_7_day_threshold_reviewed_at: null, + }); + }); + + test('snapshots only current-hour Variable Credit spend drivers for anomaly alerts', async () => { + const userId = await createUser(); + const owner = { type: 'user', id: userId } as const; + const asOf = new Date().toISOString(); + const currentHourStart = floorUtcHour(new Date(asOf)); + const priorHourStart = addHours(currentHourStart, -1); + + await db.insert(cost_insight_owner_configs).values({ + owned_by_user_id: userId, + spend_alerts_enabled: true, + cost_suggestions_enabled: false, + }); + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt: currentHourStart, + amountMicrodollars: 30_000_000, + category: 'variable', + source: 'ai_gateway', + productKey: 'cli', + featureKey: 'messages', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }); + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt: currentHourStart, + amountMicrodollars: 100_000_000, + category: 'scheduled', + source: 'kiloclaw', + productKey: 'kiloclaw', + featureKey: 'renewal', + modelOrPlanKey: 'standard', + providerKey: 'other', + }); + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt: priorHourStart, + amountMicrodollars: 200_000_000, + category: 'variable', + source: 'ai_gateway', + productKey: 'cloud-agent', + featureKey: 'responses', + modelOrPlanKey: 'openai/gpt-4.1', + providerKey: 'openai', + }); + + await evaluateCostInsightsForOwner(db, owner, { asOf }); + + const [event] = await db + .select({ snapshot: cost_insight_events.snapshot }) + .from(cost_insight_events) + .where( + and( + eq(cost_insight_events.owned_by_user_id, userId), + eq(cost_insight_events.event_type, 'anomaly_alert') + ) + ); + expect(event?.snapshot.topDriversWindow).toEqual({ + startInclusive: currentHourStart, + endExclusive: asOf, + spendCategory: 'variable', + }); + expect(event?.snapshot.topDrivers).toEqual([ + expect.objectContaining({ + spendCategory: 'variable', + productKey: 'cli', + totalMicrodollars: 30_000_000, + }), + ]); + }); +}); diff --git a/apps/web/src/lib/cost-insights/evaluation.ts b/apps/web/src/lib/cost-insights/evaluation.ts index 9b4a348374..f48b4df584 100644 --- a/apps/web/src/lib/cost-insights/evaluation.ts +++ b/apps/web/src/lib/cost-insights/evaluation.ts @@ -8,7 +8,8 @@ import { db } from '@/lib/drizzle'; import { getOwnerCurrentHourSpend, getOwnerHourlySpend, - getOwnerRolling24HourSpendExact, + getOwnerRollingDriverEvidenceExact, + getOwnerRollingSpendExact, getOwnerTopSpendDrivers, type OwnerTopSpendDriver, } from './spend-repository'; @@ -34,6 +35,7 @@ import { upsertCostInsightActiveSuggestion, type CostInsightDatabase, type CostInsightRootDatabase, + type CostInsightThresholdAlertKind, } from './repository'; import { dispatchPendingCostInsightNotifications } from './notifications'; @@ -59,9 +61,45 @@ export type CostInsightEvaluationSummary = { evaluatedAt: string; anomalyEventCreated: boolean; thresholdEventCreated: boolean; + threshold7DayEventCreated: boolean; + threshold30DayEventCreated: boolean; suggestionCreated: boolean; }; +const thresholdWindowDescriptors = { + threshold: { + windowHours: 24, + windowLabel: '24-hour', + snapshotWindow: 'rolling_24h', + rollingSnapshot: (microdollars: number) => ({ rolling24HourMicrodollars: microdollars }), + }, + threshold_7d: { + windowHours: 7 * 24, + windowLabel: '7-day', + snapshotWindow: 'rolling_7d', + rollingSnapshot: (microdollars: number) => ({ rolling7DayMicrodollars: microdollars }), + }, + threshold_30d: { + windowHours: 30 * 24, + windowLabel: '30-day', + snapshotWindow: 'rolling_30d', + rollingSnapshot: (microdollars: number) => ({ rolling30DayMicrodollars: microdollars }), + }, +} satisfies Record< + CostInsightThresholdAlertKind, + { + windowHours: number; + windowLabel: string; + snapshotWindow: 'rolling_24h' | 'rolling_7d' | 'rolling_30d'; + rollingSnapshot: ( + microdollars: number + ) => + | { rolling24HourMicrodollars: number } + | { rolling7DayMicrodollars: number } + | { rolling30DayMicrodollars: number }; + } +>; + function topDriverSnapshot(drivers: OwnerTopSpendDriver[]): AlertTopDriverSnapshot[] { return drivers.slice(0, 5).map(driver => ({ spendCategory: driver.category, @@ -101,17 +139,12 @@ function sentenceLabel(value: string): string { return value .split(/[-_:/.]+/) .filter(Boolean) - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .map(part => + part.toLowerCase() === 'cli' ? 'CLI' : part.charAt(0).toUpperCase() + part.slice(1) + ) .join(' '); } -function thirtyDayPace(microdollars: number, windowDays: number): number { - return ( - Math.round(((microdollars / Math.max(1, windowDays)) * 30) / MICRODOLLARS_PER_USD) * - MICRODOLLARS_PER_USD - ); -} - export async function getCostInsightAnomalyPolicy( database: CostInsightDatabase, owner: CostInsightSpendOwner, @@ -155,12 +188,17 @@ async function maybeCreateAnomalyAlert(params: { title: 'Spend Anomaly Alert', description: `Usage-based spend reached ${usdLabel( params.currentHourVariableMicrodollars - )} in the current UTC hour.`, + )} in the current hour.`, snapshot: { currentHourVariableMicrodollars: params.currentHourVariableMicrodollars, anomalyBaselineMicrodollars: params.anomalyPolicy.baselineMicrodollars, anomalyThresholdMicrodollars: params.anomalyPolicy.thresholdMicrodollars, topDrivers: topDriverSnapshot(params.topDrivers), + topDriversWindow: { + startInclusive: params.currentHourStart, + endExclusive: params.asOf, + spendCategory: 'variable', + }, }, dedupeKey: `anomaly:${params.currentHourStart}`, }); @@ -183,38 +221,64 @@ async function maybeCreateThresholdAlert(params: { database: CostInsightDatabase; owner: CostInsightSpendOwner; asOf: string; + alertKind: CostInsightThresholdAlertKind; thresholdMicrodollars: number | null; - rolling24HourMicrodollars: number | null; - topDrivers: OwnerTopSpendDriver[]; + rollingMicrodollars: number | null; }): Promise { if (params.thresholdMicrodollars === null) { - await clearCostInsightThresholdEpisode(params.database, params.owner, null); + await clearCostInsightThresholdEpisode(params.database, params.owner, null, params.alertKind); return false; } - if (params.rolling24HourMicrodollars === null) return false; + if (params.rollingMicrodollars === null) return false; const dashboardState = await getCostInsightDashboardState(params.database, params.owner); - if (params.rolling24HourMicrodollars < params.thresholdMicrodollars) { - if (dashboardState.state?.thresholdCrossingActive) { - await clearCostInsightThresholdEpisode(params.database, params.owner, params.asOf); + const crossingActive = (() => { + if (params.alertKind === 'threshold_7d') + return dashboardState.state?.threshold7DayCrossingActive; + if (params.alertKind === 'threshold_30d') { + return dashboardState.state?.threshold30DayCrossingActive; + } + return dashboardState.state?.thresholdCrossingActive; + })(); + if (params.rollingMicrodollars < params.thresholdMicrodollars) { + if (crossingActive) { + await clearCostInsightThresholdEpisode( + params.database, + params.owner, + params.asOf, + params.alertKind + ); } return false; } - if (dashboardState.state?.thresholdCrossingActive) return false; + if (crossingActive) return false; + const evidence = await getOwnerRollingDriverEvidenceExact(params.database, { + owner: params.owner, + asOf: params.asOf, + windowHours: thresholdWindowDescriptors[params.alertKind].windowHours, + }); + if (evidence.totalMicrodollars < params.thresholdMicrodollars) return false; + + const descriptor = thresholdWindowDescriptors[params.alertKind]; const event = await createCostInsightEvent(params.database, { owner: params.owner, eventType: 'threshold_crossed', - alertKind: 'threshold', - title: 'Spend Threshold Alert', - description: `Rolling 24-hour Credit spend crossed ${usdLabel(params.thresholdMicrodollars)}.`, + alertKind: params.alertKind, + title: `${descriptor.windowLabel} Spend Threshold Alert`, + description: `Rolling ${descriptor.windowLabel} Credit spend crossed ${usdLabel(params.thresholdMicrodollars)}.`, snapshot: { thresholdMicrodollars: params.thresholdMicrodollars, - rolling24HourMicrodollars: params.rolling24HourMicrodollars, - topDrivers: topDriverSnapshot(params.topDrivers), + thresholdWindow: descriptor.snapshotWindow, + ...descriptor.rollingSnapshot(evidence.totalMicrodollars), + topDrivers: topDriverSnapshot(evidence.topDrivers), + topDriversWindow: { + startInclusive: evidence.windowStart, + endExclusive: evidence.asOf, + }, }, - dedupeKey: `threshold:${params.thresholdMicrodollars}:${params.asOf}`, + dedupeKey: `${params.alertKind}:${params.thresholdMicrodollars}:${params.asOf}`, }); if (!event.created) return false; @@ -222,6 +286,7 @@ async function maybeCreateThresholdAlert(params: { owner: params.owner, eventId: event.id, crossedAt: params.asOf, + alertKind: params.alertKind, }); await createCostInsightNotificationDeliveries( params.database, @@ -244,9 +309,6 @@ async function maybeCreateCostSuggestion(params: { if (activeSuggestions.length > 0) return false; const topDriver = params.topDrivers[0]; - const observedPaceLabel = roundedUsdLabel( - thirtyDayPace(params.observedMicrodollars, params.evidenceWindowDays) - ); const codingPlanCandidate = topDriver && @@ -262,10 +324,6 @@ async function maybeCreateCostSuggestion(params: { topDriver.modelOrPlanKey !== 'other' ? sentenceLabel(topDriver.modelOrPlanKey) : sentenceLabel(topDriver.productKey); - const driverPaceLabel = roundedUsdLabel( - thirtyDayPace(topDriver.totalMicrodollars, params.evidenceWindowDays) - ); - return { suggestionKind: 'coding_plan' as const, suggestionKey: suggestionKey([ @@ -277,8 +335,8 @@ async function maybeCreateCostSuggestion(params: { topDriver.productKey, topDriver.modelOrPlanKey, ]), - title: `Review Coding Plan coverage for ${driverLabel}`, - description: `You spent ${usdLabel(topDriver.totalMicrodollars)} on ${driverLabel} in the last ${params.evidenceWindowDays} days, about ${driverPaceLabel} over 30 days at the same pace. A Coding Plan may improve cost efficiency for recurring model usage.`, + title: `Consider a Coding Plan for ${driverLabel}`, + description: `A Coding Plan may improve cost efficiency for recurring ${driverLabel} usage.`, ctaLabel: 'View subscriptions', ctaHref: params.owner.type === 'organization' @@ -299,13 +357,13 @@ async function maybeCreateCostSuggestion(params: { params.evidenceWindowEnd.slice(0, 10), String(params.observedMicrodollars), ]), - title: 'Get more credits from your monthly spend with Kilo Pass Expert', - description: `You spent ${usdLabel(params.observedMicrodollars)} on pay-as-you-go credits in the last ${params.evidenceWindowDays} days, about ${observedPaceLabel} over 30 days at the same pace. Kilo Pass Expert costs ${roundedUsdLabel(KILO_PASS_EXPERT_MONTHLY_MICRODOLLARS)} per month and includes ${roundedUsdLabel(KILO_PASS_EXPERT_MONTHLY_MICRODOLLARS)} in paid credits, plus up to ${usdLabel(KILO_PASS_EXPERT_BONUS_MICRODOLLARS)} in free bonus credits. Based on your recent spend, the plan could give you more credits for part of the spend you already make.`, + title: 'Get more credits with Kilo Pass Expert', + description: `The plan includes ${roundedUsdLabel(KILO_PASS_EXPERT_MONTHLY_MICRODOLLARS)} in paid credits plus up to ${usdLabel(KILO_PASS_EXPERT_BONUS_MICRODOLLARS)} in free bonus credits.`, ctaLabel: 'View Kilo Pass Expert', ctaHref: '/subscriptions/kilo-pass', observedMicrodollars: params.observedMicrodollars, benefitLabel: 'Expert plan', - benefitDetail: `${roundedUsdLabel(KILO_PASS_EXPERT_MONTHLY_MICRODOLLARS)} + up to ${usdLabel( + benefitDetail: `${roundedUsdLabel(KILO_PASS_EXPERT_MONTHLY_MICRODOLLARS)}/mo + up to ${usdLabel( KILO_PASS_EXPERT_BONUS_MICRODOLLARS )} bonus`, } @@ -357,34 +415,60 @@ async function evaluateCostInsightsForOwnerLocked( ): Promise { const asOf = options.asOf ?? new Date().toISOString(); const currentHourStart = floorUtcHour(new Date(asOf)); - const topDriverStart = addHours(currentHourStart, -24); const topDriverEnd = addHours(currentHourStart, 1); const suggestionWindowEnd = topDriverEnd; const suggestionWindowStart = addDays(suggestionWindowEnd, -7); const config = await getCostInsightOwnerConfig(database, owner); const currentHourSpend = await getOwnerCurrentHourSpend(database, owner); - const [topDrivers, suggestionTopDrivers, suggestionHourlySpend, rolling24HourSpend] = - await Promise.all([ - getOwnerTopSpendDrivers(database, { - owner, - startHour: topDriverStart, - endHourExclusive: topDriverEnd, - limit: 5, - }), - getOwnerTopSpendDrivers(database, { - owner, - startHour: suggestionWindowStart, - endHourExclusive: suggestionWindowEnd, - limit: 5, - }), - getOwnerHourlySpend(database, { - owner, - startHour: suggestionWindowStart, - endHourExclusive: suggestionWindowEnd, - }), - getOwnerRolling24HourSpendExact(database, { owner, asOf }), - ]); + const rolling30DaySpendPromise = + config?.spend_alerts_enabled && config.spend_30_day_threshold_microdollars !== null + ? getOwnerRollingSpendExact(database, { + owner, + asOf, + windowHours: 30 * 24, + fallbackToCanonical: true, + }) + : Promise.resolve({ totalMicrodollars: null }); + const rolling7DaySpendPromise = + config?.spend_alerts_enabled && config.spend_7_day_threshold_microdollars !== null + ? getOwnerRollingSpendExact(database, { + owner, + asOf, + windowHours: 7 * 24, + fallbackToCanonical: true, + }) + : Promise.resolve({ totalMicrodollars: null }); + const [ + anomalyTopDrivers, + suggestionTopDrivers, + suggestionHourlySpend, + rolling24HourSpend, + rolling7DaySpend, + rolling30DaySpend, + ] = await Promise.all([ + getOwnerTopSpendDrivers(database, { + owner, + startHour: currentHourStart, + endHourExclusive: topDriverEnd, + category: 'variable', + limit: 5, + }), + getOwnerTopSpendDrivers(database, { + owner, + startHour: suggestionWindowStart, + endHourExclusive: suggestionWindowEnd, + limit: 5, + }), + getOwnerHourlySpend(database, { + owner, + startHour: suggestionWindowStart, + endHourExclusive: suggestionWindowEnd, + }), + getOwnerRollingSpendExact(database, { owner, asOf, windowHours: 24 }), + rolling7DaySpendPromise, + rolling30DaySpendPromise, + ]); const suggestionObservedMicrodollars = suggestionHourlySpend.reduce( (sum, hour) => sum + (hour.variableMicrodollars ?? 0) + (hour.scheduledMicrodollars ?? 0), 0 @@ -392,26 +476,46 @@ async function evaluateCostInsightsForOwnerLocked( let anomalyEventCreated = false; let thresholdEventCreated = false; + let threshold7DayEventCreated = false; + let threshold30DayEventCreated = false; let suggestionCreated = false; if (config?.spend_alerts_enabled) { - const anomalyPolicy = await getCostInsightAnomalyPolicy(database, owner, currentHourStart); - anomalyEventCreated = await maybeCreateAnomalyAlert({ + if (config.anomaly_alerts_enabled) { + const anomalyPolicy = await getCostInsightAnomalyPolicy(database, owner, currentHourStart); + anomalyEventCreated = await maybeCreateAnomalyAlert({ + database, + owner, + asOf, + currentHourStart, + currentHourVariableMicrodollars: currentHourSpend.variableMicrodollars, + anomalyPolicy, + topDrivers: anomalyTopDrivers, + }); + } + thresholdEventCreated = await maybeCreateThresholdAlert({ database, owner, asOf, - currentHourStart, - currentHourVariableMicrodollars: currentHourSpend.variableMicrodollars, - anomalyPolicy, - topDrivers, + alertKind: 'threshold', + thresholdMicrodollars: config.spend_threshold_microdollars, + rollingMicrodollars: rolling24HourSpend.totalMicrodollars, }); - thresholdEventCreated = await maybeCreateThresholdAlert({ + threshold7DayEventCreated = await maybeCreateThresholdAlert({ database, owner, asOf, - thresholdMicrodollars: config.spend_threshold_microdollars, - rolling24HourMicrodollars: rolling24HourSpend.totalMicrodollars, - topDrivers, + alertKind: 'threshold_7d', + thresholdMicrodollars: config.spend_7_day_threshold_microdollars, + rollingMicrodollars: rolling7DaySpend.totalMicrodollars, + }); + threshold30DayEventCreated = await maybeCreateThresholdAlert({ + database, + owner, + asOf, + alertKind: 'threshold_30d', + thresholdMicrodollars: config.spend_30_day_threshold_microdollars, + rollingMicrodollars: rolling30DaySpend.totalMicrodollars, }); } @@ -433,6 +537,8 @@ async function evaluateCostInsightsForOwnerLocked( evaluatedAt: asOf, anomalyEventCreated, thresholdEventCreated, + threshold7DayEventCreated, + threshold30DayEventCreated, suggestionCreated, }; } diff --git a/apps/web/src/lib/cost-insights/notifications.ts b/apps/web/src/lib/cost-insights/notifications.ts index eefd1d2838..40cc9cb49e 100644 --- a/apps/web/src/lib/cost-insights/notifications.ts +++ b/apps/web/src/lib/cost-insights/notifications.ts @@ -25,6 +25,8 @@ type CostInsightClaimedDeliveryRow = { snapshot: { thresholdMicrodollars?: number | null; rolling24HourMicrodollars?: number | null; + rolling7DayMicrodollars?: number | null; + rolling30DayMicrodollars?: number | null; currentHourVariableMicrodollars?: number | null; anomalyThresholdMicrodollars?: number | null; }; @@ -58,9 +60,25 @@ function amountLabels(row: CostInsightClaimedDeliveryRow): { primaryAmountLabel: string; secondaryAmountLabel: string; } { - if (row.alert_kind === 'threshold') { + if ( + row.alert_kind === 'threshold' || + row.alert_kind === 'threshold_7d' || + row.alert_kind === 'threshold_30d' + ) { + const windowLabel = + row.alert_kind === 'threshold_7d' + ? '7-day' + : row.alert_kind === 'threshold_30d' + ? '30-day' + : '24-hour'; + const rollingMicrodollars = + row.alert_kind === 'threshold_7d' + ? row.snapshot.rolling7DayMicrodollars + : row.alert_kind === 'threshold_30d' + ? row.snapshot.rolling30DayMicrodollars + : row.snapshot.rolling24HourMicrodollars; return { - primaryAmountLabel: `Rolling 24-hour spend: ${money(row.snapshot.rolling24HourMicrodollars)}`, + primaryAmountLabel: `Rolling ${windowLabel} spend: ${money(rollingMicrodollars)}`, secondaryAmountLabel: `Spend threshold: ${money(row.snapshot.thresholdMicrodollars)}`, }; } diff --git a/apps/web/src/lib/cost-insights/posthog-tracking.test.ts b/apps/web/src/lib/cost-insights/posthog-tracking.test.ts new file mode 100644 index 0000000000..6d641290c5 --- /dev/null +++ b/apps/web/src/lib/cost-insights/posthog-tracking.test.ts @@ -0,0 +1,258 @@ +import { beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import type { + trackCostInsightsAlertAction as trackCostInsightsAlertActionType, + trackCostInsightsSettingsSaved as trackCostInsightsSettingsSavedType, + trackCostInsightsSuggestionAction as trackCostInsightsSuggestionActionType, + trackCostInsightsUiInteraction as trackCostInsightsUiInteractionType, +} from './posthog-tracking'; +import { CostInsightsUiInteractionSchema } from './tracking'; + +jest.mock('@/lib/posthog', () => { + const mockCapture = jest.fn(); + return { + __esModule: true, + default: jest.fn(() => ({ capture: mockCapture })), + mockCapture, + }; +}); + +jest.mock('@sentry/nextjs', () => { + const mockCaptureException = jest.fn(); + return { + captureException: mockCaptureException, + mockCaptureException, + }; +}); + +let trackCostInsightsAlertAction: typeof trackCostInsightsAlertActionType; +let trackCostInsightsSettingsSaved: typeof trackCostInsightsSettingsSavedType; +let trackCostInsightsSuggestionAction: typeof trackCostInsightsSuggestionActionType; +let trackCostInsightsUiInteraction: typeof trackCostInsightsUiInteractionType; + +const posthogMock: { mockCapture: jest.Mock } = jest.requireMock('@/lib/posthog'); +const sentryMock: { mockCaptureException: jest.Mock } = jest.requireMock('@sentry/nextjs'); +const { mockCapture } = posthogMock; +const { mockCaptureException } = sentryMock; + +beforeAll(async () => { + ({ + trackCostInsightsAlertAction, + trackCostInsightsSettingsSaved, + trackCostInsightsSuggestionAction, + trackCostInsightsUiInteraction, + } = await import('./posthog-tracking')); +}); + +const personalContext = { + distinctId: 'user-123', + userId: 'user-123', + ownerType: 'personal', + authorizedRole: 'personal', +} as const; + +describe('Cost Insights PostHog tracking', () => { + beforeEach(() => { + mockCapture.mockReset(); + mockCaptureException.mockReset(); + }); + + it('captures dashboard views with only allowlisted properties', () => { + const interaction = { + interaction: 'dashboard_viewed', + spendAlertsEnabled: false, + hasActiveAlert: true, + hasActiveSuggestion: true, + question: 'must-not-leak', + thresholdUsd: '250.00', + } as const; + + trackCostInsightsUiInteraction(personalContext, interaction); + + expect(mockCapture).toHaveBeenCalledWith({ + distinctId: 'user-123', + event: 'cost_insights_ui_interaction', + properties: { + interaction: 'dashboard_viewed', + feature: 'cost-insights', + operation: 'ui_interaction', + userId: 'user-123', + ownerType: 'personal', + authorizedRole: 'personal', + spendAlertsEnabled: false, + hasActiveAlert: true, + hasActiveSuggestion: true, + }, + }); + }); + + it('captures organization range selection with trusted context', () => { + trackCostInsightsUiInteraction( + { + distinctId: 'user-456', + userId: 'user-456', + ownerType: 'organization', + organizationId: 'organization-123', + authorizedRole: 'billing_manager', + }, + { interaction: 'spend_range_selected', range: '30d' } + ); + + expect(mockCapture).toHaveBeenCalledWith({ + distinctId: 'user-456', + event: 'cost_insights_ui_interaction', + properties: { + interaction: 'spend_range_selected', + feature: 'cost-insights', + operation: 'ui_interaction', + userId: 'user-456', + ownerType: 'organization', + authorizedRole: 'billing_manager', + organizationId: 'organization-123', + range: '30d', + }, + }); + }); + + it('captures settings transitions without exact financial values', () => { + const event = { + ...personalContext, + spendAlertsTransition: 'enabled', + anomalyAlertsTransition: 'unchanged', + costSuggestionsTransition: 'disabled', + threshold24hTransition: 'changed', + threshold30dTransition: 'added', + spendAlertsEnabled: true, + anomalyAlertsEnabled: true, + costSuggestionsEnabled: false, + threshold24hConfigured: true, + threshold30dConfigured: true, + spendThresholdMicrodollars: 250_000_000, + } as const; + + trackCostInsightsSettingsSaved(event); + + expect(mockCapture).toHaveBeenCalledWith({ + distinctId: 'user-123', + event: 'cost_insights_settings_saved', + properties: { + phase: 'accepted', + spendAlertsTransition: 'enabled', + anomalyAlertsTransition: 'unchanged', + costSuggestionsTransition: 'disabled', + threshold24hTransition: 'changed', + threshold30dTransition: 'added', + spendAlertsEnabled: true, + anomalyAlertsEnabled: true, + costSuggestionsEnabled: false, + threshold24hConfigured: true, + threshold30dConfigured: true, + feature: 'cost-insights', + operation: 'save_settings', + userId: 'user-123', + ownerType: 'personal', + authorizedRole: 'personal', + }, + }); + }); + + it('captures accepted 30-day alert acknowledgment', () => { + trackCostInsightsAlertAction({ + ...personalContext, + action: 'acknowledge', + alertKind: 'threshold_30d', + }); + + expect(mockCapture).toHaveBeenCalledWith({ + distinctId: 'user-123', + event: 'cost_insights_alert_action', + properties: { + action: 'acknowledge', + alertKind: 'threshold_30d', + phase: 'accepted', + feature: 'cost-insights', + operation: 'alert_action', + userId: 'user-123', + ownerType: 'personal', + authorizedRole: 'personal', + }, + }); + }); + + it.each([ + { action: 'open_cta' as const, phase: 'clicked' as const }, + { action: 'dismiss' as const, phase: 'accepted' as const }, + ])('captures suggestion action $action without suggestion identity', ({ action, phase }) => { + const event = { + ...personalContext, + action, + phase, + suggestionKind: 'kilo_pass', + suggestionId: 'must-not-leak', + ctaHref: 'must-not-leak', + } as const; + + trackCostInsightsSuggestionAction(event); + + expect(mockCapture).toHaveBeenCalledWith({ + distinctId: 'user-123', + event: 'cost_insights_suggestion_action', + properties: { + action, + suggestionKind: 'kilo_pass', + phase, + feature: 'cost-insights', + operation: 'suggestion_action', + userId: 'user-123', + ownerType: 'personal', + authorizedRole: 'personal', + }, + }); + }); + + it('reports capture failures without throwing or leaking arbitrary properties', () => { + const error = new Error('capture failed'); + mockCapture.mockImplementation(() => { + throw error; + }); + + expect(() => + trackCostInsightsUiInteraction(personalContext, { + interaction: 'ask_kilo_question_submitted', + source: 'follow_up', + experience: 'ui_only', + }) + ).not.toThrow(); + expect(mockCaptureException).toHaveBeenCalledWith(error, { + tags: { source: 'posthog_cost_insights_ui_interaction' }, + extra: { + properties: { + interaction: 'ask_kilo_question_submitted', + feature: 'cost-insights', + operation: 'ui_interaction', + userId: 'user-123', + ownerType: 'personal', + authorizedRole: 'personal', + source: 'follow_up', + experience: 'ui_only', + }, + }, + }); + }); + + it('rejects free text and mismatched interaction properties at the router boundary', () => { + expect( + CostInsightsUiInteractionSchema.safeParse({ + interaction: 'ask_kilo_question_submitted', + source: 'follow_up', + experience: 'ui_only', + question: 'private spend question', + }).success + ).toBe(false); + expect( + CostInsightsUiInteractionSchema.safeParse({ + interaction: 'spend_range_selected', + range: '365d', + }).success + ).toBe(false); + }); +}); diff --git a/apps/web/src/lib/cost-insights/posthog-tracking.ts b/apps/web/src/lib/cost-insights/posthog-tracking.ts new file mode 100644 index 0000000000..d00869b04c --- /dev/null +++ b/apps/web/src/lib/cost-insights/posthog-tracking.ts @@ -0,0 +1,201 @@ +import 'server-only'; + +import { captureException } from '@sentry/nextjs'; + +import PostHogClient from '@/lib/posthog'; +import type { CostInsightsUiInteraction } from './tracking'; + +export type CostInsightsAuthorizedRole = 'personal' | 'owner' | 'billing_manager' | 'admin'; + +export type CostInsightsTrackingContext = { + distinctId: string; + userId: string; + ownerType: 'personal' | 'organization'; + authorizedRole: CostInsightsAuthorizedRole; + organizationId?: string; +}; + +type ConfigTransition = 'enabled' | 'disabled' | 'unchanged'; +type ThresholdTransition = 'added' | 'changed' | 'removed' | 'unchanged'; + +type CostInsightsSettingsSavedEvent = CostInsightsTrackingContext & { + spendAlertsTransition: ConfigTransition; + anomalyAlertsTransition: ConfigTransition; + costSuggestionsTransition: ConfigTransition; + threshold24hTransition: ThresholdTransition; + threshold7dTransition?: ThresholdTransition; + threshold30dTransition: ThresholdTransition; + spendAlertsEnabled: boolean; + anomalyAlertsEnabled: boolean; + costSuggestionsEnabled: boolean; + threshold24hConfigured: boolean; + threshold7dConfigured?: boolean; + threshold30dConfigured: boolean; +}; + +type CostInsightsAlertActionEvent = CostInsightsTrackingContext & { + action: 'acknowledge'; + alertKind: 'anomaly' | 'threshold' | 'threshold_7d' | 'threshold_30d'; +}; + +type CostInsightsSuggestionActionEvent = CostInsightsTrackingContext & { + action: 'open_cta' | 'dismiss'; + suggestionKind: 'coding_plan' | 'kilo_pass'; + phase: 'clicked' | 'accepted'; +}; + +const posthogClient = PostHogClient(); + +function contextProperties(context: CostInsightsTrackingContext) { + return { + userId: context.userId, + ownerType: context.ownerType, + authorizedRole: context.authorizedRole, + ...(context.organizationId === undefined ? {} : { organizationId: context.organizationId }), + }; +} + +function captureCostInsightsEvent(params: { + distinctId: string; + event: string; + source: string; + properties: Record; +}): void { + try { + posthogClient.capture({ + distinctId: params.distinctId, + event: params.event, + properties: params.properties, + }); + } catch (error) { + captureException(error, { + tags: { source: params.source }, + extra: { properties: params.properties }, + }); + } +} + +export function trackCostInsightsUiInteraction( + context: CostInsightsTrackingContext, + interaction: CostInsightsUiInteraction +): void { + const interactionProperties = (() => { + switch (interaction.interaction) { + case 'dashboard_viewed': + return { + spendAlertsEnabled: interaction.spendAlertsEnabled, + hasActiveAlert: interaction.hasActiveAlert, + hasActiveSuggestion: interaction.hasActiveSuggestion, + }; + case 'settings_viewed': + return { + spendAlertsEnabled: interaction.spendAlertsEnabled, + costSuggestionsEnabled: interaction.costSuggestionsEnabled, + threshold24hConfigured: interaction.threshold24hConfigured, + threshold7dConfigured: interaction.threshold7dConfigured, + threshold30dConfigured: interaction.threshold30dConfigured, + readOnly: interaction.readOnly, + }; + case 'spend_range_selected': + return { range: interaction.range }; + case 'alert_drivers_expanded': + return { alertKind: interaction.alertKind }; + case 'alert_settings_clicked': + return { action: interaction.action }; + case 'activity_filter_selected': + return { filter: interaction.filter }; + case 'activity_page_selected': + return { direction: interaction.direction }; + case 'ask_kilo_question_submitted': + return { source: interaction.source, experience: interaction.experience }; + case 'activity_viewed': + case 'ask_kilo_viewed': + case 'setup_alerts_clicked': + return {}; + } + })(); + const properties = { + interaction: interaction.interaction, + feature: 'cost-insights', + operation: 'ui_interaction', + ...contextProperties(context), + ...interactionProperties, + }; + + captureCostInsightsEvent({ + distinctId: context.distinctId, + event: 'cost_insights_ui_interaction', + source: 'posthog_cost_insights_ui_interaction', + properties, + }); +} + +export function trackCostInsightsSettingsSaved(properties: CostInsightsSettingsSavedEvent): void { + const eventProperties = { + phase: 'accepted', + spendAlertsTransition: properties.spendAlertsTransition, + anomalyAlertsTransition: properties.anomalyAlertsTransition, + costSuggestionsTransition: properties.costSuggestionsTransition, + threshold24hTransition: properties.threshold24hTransition, + ...(properties.threshold7dTransition === undefined + ? {} + : { threshold7dTransition: properties.threshold7dTransition }), + threshold30dTransition: properties.threshold30dTransition, + spendAlertsEnabled: properties.spendAlertsEnabled, + anomalyAlertsEnabled: properties.anomalyAlertsEnabled, + costSuggestionsEnabled: properties.costSuggestionsEnabled, + threshold24hConfigured: properties.threshold24hConfigured, + ...(properties.threshold7dConfigured === undefined + ? {} + : { threshold7dConfigured: properties.threshold7dConfigured }), + threshold30dConfigured: properties.threshold30dConfigured, + feature: 'cost-insights', + operation: 'save_settings', + ...contextProperties(properties), + }; + + captureCostInsightsEvent({ + distinctId: properties.distinctId, + event: 'cost_insights_settings_saved', + source: 'posthog_cost_insights_settings_saved', + properties: eventProperties, + }); +} + +export function trackCostInsightsAlertAction(properties: CostInsightsAlertActionEvent): void { + const eventProperties = { + action: properties.action, + alertKind: properties.alertKind, + phase: 'accepted', + feature: 'cost-insights', + operation: 'alert_action', + ...contextProperties(properties), + }; + + captureCostInsightsEvent({ + distinctId: properties.distinctId, + event: 'cost_insights_alert_action', + source: 'posthog_cost_insights_alert_action', + properties: eventProperties, + }); +} + +export function trackCostInsightsSuggestionAction( + properties: CostInsightsSuggestionActionEvent +): void { + const eventProperties = { + action: properties.action, + suggestionKind: properties.suggestionKind, + phase: properties.phase, + feature: 'cost-insights', + operation: 'suggestion_action', + ...contextProperties(properties), + }; + + captureCostInsightsEvent({ + distinctId: properties.distinctId, + event: 'cost_insights_suggestion_action', + source: 'posthog_cost_insights_suggestion_action', + properties: eventProperties, + }); +} diff --git a/apps/web/src/lib/cost-insights/presenter.test.ts b/apps/web/src/lib/cost-insights/presenter.test.ts index e06fcba531..fb9bc69795 100644 --- a/apps/web/src/lib/cost-insights/presenter.test.ts +++ b/apps/web/src/lib/cost-insights/presenter.test.ts @@ -4,9 +4,20 @@ import { formatActiveCostInsightAlerts, formatActiveCostInsightSuggestions, formatCostInsightEvents, + spendRangeStartHour, } from './presenter'; describe('Cost Insights presenter', () => { + it('uses matching UTC bucket windows for every selectable spend range', () => { + const endHourExclusive = '2026-06-26T12:00:00.000Z'; + + expect(spendRangeStartHour('1h', endHourExclusive)).toBe('2026-06-26T11:00:00.000Z'); + expect(spendRangeStartHour('24h', endHourExclusive)).toBe('2026-06-25T12:00:00.000Z'); + expect(spendRangeStartHour('7d', endHourExclusive)).toBe('2026-06-19T12:00:00.000Z'); + expect(spendRangeStartHour('30d', endHourExclusive)).toBe('2026-05-27T12:00:00.000Z'); + expect(spendRangeStartHour('90d', endHourExclusive)).toBe('2026-03-28T12:00:00.000Z'); + }); + it('formats active alert cards with Storybook labels, facts, and actions', () => { const state = { state: { @@ -25,6 +36,24 @@ describe('Cost Insights presenter', () => { currentHourVariableMicrodollars: 112_700_000, anomalyBaselineMicrodollars: 6_000_000, anomalyThresholdMicrodollars: 18_000_000, + topDrivers: [ + { + spendCategory: 'variable', + source: 'ai_gateway', + productKey: 'cli', + featureKey: 'messages', + modelOrPlanKey: 'claude-sonnet-4', + providerKey: 'anthropic', + actorUserId: null, + totalMicrodollars: 74_200_000, + spendRecordCount: 184, + }, + ], + topDriversWindow: { + startInclusive: '2026-06-25T19:00:00.000Z', + endExclusive: '2026-06-25T20:00:00.000Z', + spendCategory: 'variable', + }, }, }, { @@ -32,12 +61,29 @@ describe('Cost Insights presenter', () => { snapshot: { rolling24HourMicrodollars: 184_900_000, thresholdMicrodollars: 150_000_000, + topDrivers: [ + { + spendCategory: 'scheduled', + source: 'kiloclaw', + productKey: 'kiloclaw_hosting', + featureKey: 'renewal', + modelOrPlanKey: 'standard', + providerKey: 'other', + actorUserId: null, + totalMicrodollars: 63_900_000, + spendRecordCount: 1, + }, + ], + topDriversWindow: { + startInclusive: '2026-06-24T19:02:00.000Z', + endExclusive: '2026-06-25T19:02:00.000Z', + }, }, }, ], } as Parameters[0]; - expect(formatActiveCostInsightAlerts(state)).toEqual([ + expect(formatActiveCostInsightAlerts(state, { type: 'user', id: 'personal-owner' })).toEqual([ { type: 'anomaly', title: 'Spend is unusually high this hour', @@ -47,6 +93,26 @@ describe('Cost Insights presenter', () => { { label: 'Typical hour', value: '$6.00' }, { label: 'Alert level', value: '$18.00' }, ], + driverEvidence: { + title: 'Top Variable Credit spend drivers', + description: 'Captured when the alert fired.', + periodStart: '2026-06-25T19:00:00.000Z', + periodEndExclusive: '2026-06-25T20:00:00.000Z', + drivers: [ + { + id: '["variable","ai_gateway","cli","messages","claude-sonnet-4","anthropic",null]', + label: 'CLI: Messages', + source: 'ai_gateway', + actorLabel: undefined, + modelOrProvider: 'claude-sonnet-4', + category: 'Variable Credit spend', + spendUsd: 74.2, + requestCount: 184, + }, + ], + totalSpendUsd: 112.7, + scope: 'current_hour', + }, actions: ['acknowledge', 'view_spend'], }, { @@ -58,7 +124,143 @@ describe('Cost Insights presenter', () => { { label: 'Threshold', value: '$150.00' }, { label: 'Amount over', value: '$34.90' }, ], - actions: ['acknowledge', 'adjust_threshold', 'disable_threshold'], + driverEvidence: { + title: 'Top rolling 24-hour spend drivers', + description: 'Captured when the threshold was crossed.', + periodStart: '2026-06-24T19:02:00.000Z', + periodEndExclusive: '2026-06-25T19:02:00.000Z', + drivers: [ + { + id: '["scheduled","kiloclaw","kiloclaw_hosting","renewal","standard","other",null]', + label: 'Kiloclaw Hosting: Renewal', + source: 'kiloclaw', + actorLabel: undefined, + modelOrProvider: 'standard', + category: 'Scheduled Credit spend', + spendUsd: 63.9, + requestCount: 1, + }, + ], + totalSpendUsd: 184.9, + scope: 'rolling_24h', + }, + actions: ['acknowledge', 'view_spend', 'manage_threshold'], + }, + ]); + }); + + it('formats an independent rolling 30-day threshold alert', () => { + const state = { + state: { + activeAnomalyEventId: null, + activeAnomalyHourStart: null, + activeAnomalyReviewedAt: null, + activeThresholdEventId: null, + thresholdCrossingActive: false, + thresholdReviewedAt: null, + active7DayThresholdEventId: null, + threshold7DayCrossingActive: false, + threshold7DayReviewedAt: null, + active30DayThresholdEventId: 'evt-threshold-30d', + threshold30DayCrossingActive: true, + threshold30DayReviewedAt: null, + lastEvaluatedAt: '2026-06-25T19:02:00.000Z', + }, + events: [ + { + id: 'evt-threshold-30d', + owned_by_user_id: 'personal-owner', + owned_by_organization_id: null, + event_type: 'threshold_crossed', + alert_kind: 'threshold_30d', + suggestion_kind: null, + active_suggestion_id: null, + actor_user_id: null, + title: '30-day Spend Threshold Alert', + description: 'Rolling 30-day Credit spend crossed $1,000.00.', + snapshot: { + thresholdWindow: 'rolling_30d', + rolling30DayMicrodollars: 1_250_000_000, + thresholdMicrodollars: 1_000_000_000, + topDrivers: [], + }, + dedupe_key: 'threshold_30d:1000000000:2026-06-25T19:02:00.000Z', + occurred_at: '2026-06-25T19:02:00.000Z', + created_at: '2026-06-25T19:02:00.000Z', + }, + ], + } as Parameters[0]; + + expect(formatActiveCostInsightAlerts(state, { type: 'user', id: 'personal-owner' })).toEqual([ + { + type: 'threshold_30d', + title: '30-day spend threshold crossed', + description: 'Spend reached $1,250.00 against the $1,000.00 threshold.', + facts: [ + { label: 'Last 30 days', value: '$1,250.00' }, + { label: 'Threshold', value: '$1,000.00' }, + { label: 'Amount over', value: '$250.00' }, + ], + driverEvidence: undefined, + actions: ['acknowledge', 'manage_threshold'], + }, + ]); + }); + + it('formats an independent rolling 7-day threshold alert', () => { + const state = { + state: { + activeAnomalyEventId: null, + activeAnomalyHourStart: null, + activeAnomalyReviewedAt: null, + activeThresholdEventId: null, + thresholdCrossingActive: false, + thresholdReviewedAt: null, + active7DayThresholdEventId: 'evt-threshold-7d', + threshold7DayCrossingActive: true, + threshold7DayReviewedAt: null, + active30DayThresholdEventId: null, + threshold30DayCrossingActive: false, + threshold30DayReviewedAt: null, + lastEvaluatedAt: '2026-06-25T19:02:00.000Z', + }, + events: [ + { + id: 'evt-threshold-7d', + owned_by_user_id: 'personal-owner', + owned_by_organization_id: null, + event_type: 'threshold_crossed', + alert_kind: 'threshold_7d', + suggestion_kind: null, + active_suggestion_id: null, + actor_user_id: null, + title: '7-day Spend Threshold Alert', + description: 'Rolling 7-day Credit spend crossed $500.00.', + snapshot: { + thresholdWindow: 'rolling_7d', + rolling7DayMicrodollars: 620_000_000, + thresholdMicrodollars: 500_000_000, + topDrivers: [], + }, + dedupe_key: 'threshold_7d:500000000:2026-06-25T19:02:00.000Z', + occurred_at: '2026-06-25T19:02:00.000Z', + created_at: '2026-06-25T19:02:00.000Z', + }, + ], + } as Parameters[0]; + + expect(formatActiveCostInsightAlerts(state, { type: 'user', id: 'personal-owner' })).toEqual([ + { + type: 'threshold_7d', + title: '7-day spend threshold crossed', + description: 'Spend reached $620.00 against the $500.00 threshold.', + facts: [ + { label: 'Last 7 days', value: '$620.00' }, + { label: 'Threshold', value: '$500.00' }, + { label: 'Amount over', value: '$120.00' }, + ], + driverEvidence: undefined, + actions: ['acknowledge', 'manage_threshold'], }, ]); }); @@ -68,14 +270,14 @@ describe('Cost Insights presenter', () => { { id: 'suggestion-kilo-pass', suggestion_kind: 'kilo_pass', - title: 'Get more credits from your monthly spend with Kilo Pass Expert', + title: 'Get more credits with Kilo Pass Expert', description: - 'You spent $106.90 on pay-as-you-go credits in the last 7 days, about $458 over 30 days at the same pace. Kilo Pass Expert costs $199 per month and includes $199 in paid credits, plus up to $79.60 in free bonus credits. Based on your recent spend, the plan could give you more credits for part of the spend you already make.', + 'The plan includes $199 in paid credits plus up to $79.60 in free bonus credits.', evidence_window_start: '2026-06-18T19:00:00.000Z', evidence_window_end: '2026-06-25T19:00:00.000Z', observed_microdollars: 106_900_000, benefit_label: 'Expert plan', - benefit_detail: '$199 + up to $79.60 bonus', + benefit_detail: '$199/month + up to $79.60 bonus', cta_label: 'View Kilo Pass Expert', cta_href: '/subscriptions/kilo-pass', }, @@ -86,13 +288,13 @@ describe('Cost Insights presenter', () => { id: 'suggestion-kilo-pass', type: 'kilo_pass', eyebrow: 'Cost Suggestion', - title: 'Get more credits from your monthly spend with Kilo Pass Expert', + title: 'Get more credits with Kilo Pass Expert', description: - 'You spent $106.90 on pay-as-you-go credits in the last 7 days, about $458 over 30 days at the same pace. Kilo Pass Expert costs $199 per month and includes $199 in paid credits, plus up to $79.60 in free bonus credits. Based on your recent spend, the plan could give you more credits for part of the spend you already make.', + 'The plan includes $199 in paid credits plus up to $79.60 in free bonus credits.', facts: [ { label: 'Last 7 days', value: '$106.90' }, { label: '30-day pace', value: '~$458' }, - { label: 'Expert plan', value: '$199 + up to $79.60 bonus' }, + { label: 'Expert plan', value: '$199/mo + up to $79.60 bonus' }, ], ctaLabel: 'View Kilo Pass Expert', ctaHref: '/subscriptions/kilo-pass', @@ -137,6 +339,7 @@ describe('Cost Insights presenter', () => { new Map([['member-1', 'Current Member']]) ); + expect(event?.occurredAt).toBe('2026-06-25T19:02:00.000Z'); expect(event?.topDrivers).toEqual([ { id: '["variable","ai_gateway","kilo_code","chat","claude-sonnet-4","anthropic","member-1"]', diff --git a/apps/web/src/lib/cost-insights/presenter.ts b/apps/web/src/lib/cost-insights/presenter.ts index 3bda8b91a9..d565d6358a 100644 --- a/apps/web/src/lib/cost-insights/presenter.ts +++ b/apps/web/src/lib/cost-insights/presenter.ts @@ -42,6 +42,7 @@ import { } from './spend-repository'; const rangeHours = { + '1h': 1, '24h': 24, '7d': 24 * 7, '30d': 24 * 30, @@ -57,6 +58,37 @@ const sourceDisplay = { const MS_PER_DAY = 24 * 60 * 60 * 1000; +const thresholdAlertPresentation = { + threshold: { + alertType: 'threshold', + windowLabel: '24-hour', + factLabel: 'Last 24 hours', + rollingMicrodollars: (snapshot: ListedCostInsightEvent['snapshot']) => + snapshot.rolling24HourMicrodollars ?? null, + scope: 'rolling_24h', + }, + threshold_7d: { + alertType: 'threshold_7d', + windowLabel: '7-day', + factLabel: 'Last 7 days', + rollingMicrodollars: (snapshot: ListedCostInsightEvent['snapshot']) => + snapshot.rolling7DayMicrodollars ?? null, + scope: 'rolling_7d', + }, + threshold_30d: { + alertType: 'threshold_30d', + windowLabel: '30-day', + factLabel: 'Last 30 days', + rollingMicrodollars: (snapshot: ListedCostInsightEvent['snapshot']) => + snapshot.rolling30DayMicrodollars ?? null, + scope: 'rolling_30d', + }, +} as const; + +export function spendRangeStartHour(range: SpendRange, endHourExclusive: string): string { + return addHours(endHourExclusive, -rangeHours[range]); +} + function money(microdollars: number | null): string { if (microdollars === null) return 'Unavailable'; return new Intl.NumberFormat('en-US', { @@ -89,13 +121,15 @@ function sentenceLabel(value: string): string { return value .split(/[-_:/.]+/) .filter(Boolean) - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .map(part => + part.toLowerCase() === 'cli' ? 'CLI' : part.charAt(0).toUpperCase() + part.slice(1) + ) .join(' '); } function formatHourLabel(timestamp: string, range: SpendRange): string { const date = new Date(timestamp); - if (range === '24h') { + if (range === '1h' || range === '24h') { return new Intl.DateTimeFormat('en-US', { hour: '2-digit', hourCycle: 'h23', @@ -125,9 +159,10 @@ function suggestionWindowDays(start: string, end: string): number { } function hourlyEvidence(points: OwnerHourlySpend[], range: SpendRange): SpendEvidencePoint[] { - if (range === '24h' || range === '7d') { + if (range === '1h' || range === '24h' || range === '7d') { return points.map(point => ({ label: formatHourLabel(point.hourStart, range), + periodStart: point.hourStart, variableUsd: microdollarsToUsd(point.variableMicrodollars ?? 0), scheduledUsd: microdollarsToUsd(point.scheduledMicrodollars ?? 0), })); @@ -171,11 +206,39 @@ async function loadRangeEvidence( range: SpendRange, endHourExclusive: string ): Promise { - const startHour = addHours(endHourExclusive, -rangeHours[range]); + const startHour = spendRangeStartHour(range, endHourExclusive); const points = await getOwnerHourlySpend(database, { owner, startHour, endHourExclusive }); return hourlyEvidence(points, range); } +async function loadTopDriversByRange( + database: CostInsightDatabase, + owner: CostInsightSpendOwner, + endHourExclusive: string +): Promise> { + const loadRange = (range: SpendRange) => + getOwnerTopSpendDrivers(database, { + owner, + startHour: spendRangeStartHour(range, endHourExclusive), + endHourExclusive, + limit: 5, + }); + const [thisHour, last24Hours, last7Days, last30Days, last90Days] = await Promise.all([ + loadRange('1h'), + loadRange('24h'), + loadRange('7d'), + loadRange('30d'), + loadRange('90d'), + ]); + return { + '1h': thisHour, + '24h': last24Hours, + '7d': last7Days, + '30d': last30Days, + '90d': last90Days, + }; +} + async function loadActorLabels(database: CostInsightDatabase, actorUserIds: string[]) { const ids = [...new Set(actorUserIds)].filter(Boolean).sort(); if (ids.length === 0) return new Map(); @@ -294,11 +357,40 @@ function buildMetrics(params: { } export function formatActiveCostInsightAlerts( - state: Awaited> + state: Awaited>, + owner: CostInsightSpendOwner, + actorLabels: ReadonlyMap = new Map() ): DashboardAlert[] { const alerts: DashboardAlert[] = []; for (const event of state.events) { if (event.event_type === 'anomaly_alert' && !state.state?.activeAnomalyReviewedAt) { + const snapshotDrivers = event.snapshot.topDrivers ?? []; + const drivers = mapSnapshotDrivers(owner, snapshotDrivers, actorLabels); + const driverWindow = event.snapshot.topDriversWindow; + const currentHourEvidence = driverWindow?.spendCategory === 'variable'; + const driverEvidence = + drivers.length > 0 + ? { + title: currentHourEvidence + ? 'Top Variable Credit spend drivers' + : 'Spend drivers captured with this alert', + description: currentHourEvidence + ? 'Captured when the alert fired.' + : 'Exact alert-hour scope is unavailable for this older alert.', + ...(driverWindow + ? { + periodStart: driverWindow.startInclusive, + periodEndExclusive: driverWindow.endExclusive, + } + : {}), + drivers, + totalSpendUsd: microdollarsToUsd( + event.snapshot.currentHourVariableMicrodollars ?? + snapshotDrivers.reduce((sum, driver) => sum + driver.totalMicrodollars, 0) + ), + scope: currentHourEvidence ? ('current_hour' as const) : ('legacy' as const), + } + : undefined; alerts.push({ type: 'anomaly', title: 'Spend is unusually high this hour', @@ -317,26 +409,62 @@ export function formatActiveCostInsightAlerts( value: moneyWithCents(event.snapshot.anomalyThresholdMicrodollars ?? null), }, ], - actions: ['acknowledge', 'view_spend'], + driverEvidence, + actions: driverEvidence ? ['acknowledge', 'view_spend'] : ['acknowledge'], }); } - if (event.event_type === 'threshold_crossed' && !state.state?.thresholdReviewedAt) { - const rolling24HourMicrodollars = event.snapshot.rolling24HourMicrodollars ?? null; + if (event.event_type === 'threshold_crossed') { + const alertKind = event.alert_kind ?? 'threshold'; + if ( + alertKind !== 'threshold' && + alertKind !== 'threshold_7d' && + alertKind !== 'threshold_30d' + ) { + continue; + } + const presentation = thresholdAlertPresentation[alertKind]; + const reviewedAt = + alertKind === 'threshold_7d' + ? state.state?.threshold7DayReviewedAt + : alertKind === 'threshold_30d' + ? state.state?.threshold30DayReviewedAt + : state.state?.thresholdReviewedAt; + if (reviewedAt) continue; + + const rollingMicrodollars = presentation.rollingMicrodollars(event.snapshot); const thresholdMicrodollars = event.snapshot.thresholdMicrodollars ?? null; const amountOverMicrodollars = - rolling24HourMicrodollars === null || thresholdMicrodollars === null + rollingMicrodollars === null || thresholdMicrodollars === null ? null - : Math.max(0, rolling24HourMicrodollars - thresholdMicrodollars); + : Math.max(0, rollingMicrodollars - thresholdMicrodollars); + const snapshotDrivers = event.snapshot.topDrivers ?? []; + const drivers = mapSnapshotDrivers(owner, snapshotDrivers, actorLabels); + const driverWindow = event.snapshot.topDriversWindow; + const driverEvidence = + drivers.length > 0 && driverWindow && driverWindow.spendCategory === undefined + ? { + title: `Top rolling ${presentation.windowLabel} spend drivers`, + description: 'Captured when the threshold was crossed.', + periodStart: driverWindow.startInclusive, + periodEndExclusive: driverWindow.endExclusive, + drivers, + totalSpendUsd: microdollarsToUsd( + rollingMicrodollars ?? + snapshotDrivers.reduce((sum, driver) => sum + driver.totalMicrodollars, 0) + ), + scope: presentation.scope, + } + : undefined; alerts.push({ - type: 'threshold', - title: '24-hour spend threshold crossed', + type: presentation.alertType, + title: `${presentation.windowLabel} spend threshold crossed`, description: `Spend reached ${moneyWithCents( - rolling24HourMicrodollars + rollingMicrodollars )} against the ${moneyWithCents(thresholdMicrodollars)} threshold.`, facts: [ { - label: 'Last 24 hours', - value: moneyWithCents(rolling24HourMicrodollars), + label: presentation.factLabel, + value: moneyWithCents(rollingMicrodollars), }, { label: 'Threshold', @@ -347,7 +475,10 @@ export function formatActiveCostInsightAlerts( value: moneyWithCents(amountOverMicrodollars), }, ], - actions: ['acknowledge', 'adjust_threshold', 'disable_threshold'], + driverEvidence, + actions: driverEvidence + ? ['acknowledge', 'view_spend', 'manage_threshold'] + : ['acknowledge', 'manage_threshold'], }); } } @@ -367,7 +498,10 @@ export function formatActiveCostInsightSuggestions( MICRODOLLARS_PER_USD; const planFact = suggestion.suggestion_kind === 'kilo_pass' - ? { label: 'Expert plan', value: suggestion.benefit_detail } + ? { + label: 'Expert plan', + value: suggestion.benefit_detail.replace('/month', '/mo'), + } : { label: suggestion.benefit_label, value: suggestion.benefit_detail }; return { @@ -445,27 +579,28 @@ export function formatCostInsightEvents( type: event.eventType === 'alert_reviewed' ? 'reviewed' : event.eventType, title: event.title, description: event.description, - timestampLabel: new Intl.DateTimeFormat('en-US', { - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - timeZone: 'UTC', - timeZoneName: 'short', - }).format(new Date(event.occurredAt)), + occurredAt: event.occurredAt, actorLabel: event.actorName ?? undefined, amountLabel: - event.snapshot.rolling24HourMicrodollars !== undefined - ? money(event.snapshot.rolling24HourMicrodollars ?? null) - : event.snapshot.currentHourVariableMicrodollars !== undefined - ? money(event.snapshot.currentHourVariableMicrodollars ?? null) - : undefined, + event.snapshot.rolling30DayMicrodollars !== undefined + ? money(event.snapshot.rolling30DayMicrodollars ?? null) + : event.snapshot.rolling7DayMicrodollars !== undefined + ? money(event.snapshot.rolling7DayMicrodollars ?? null) + : event.snapshot.rolling24HourMicrodollars !== undefined + ? money(event.snapshot.rolling24HourMicrodollars ?? null) + : event.snapshot.currentHourVariableMicrodollars !== undefined + ? money(event.snapshot.currentHourVariableMicrodollars ?? null) + : undefined, amountClassifier: - event.snapshot.rolling24HourMicrodollars !== undefined - ? 'rolling 24h' - : event.snapshot.currentHourVariableMicrodollars !== undefined - ? 'current hour' - : undefined, + event.snapshot.rolling30DayMicrodollars !== undefined + ? 'rolling 30d' + : event.snapshot.rolling7DayMicrodollars !== undefined + ? 'rolling 7d' + : event.snapshot.rolling24HourMicrodollars !== undefined + ? 'rolling 24h' + : event.snapshot.currentHourVariableMicrodollars !== undefined + ? 'current hour' + : undefined, topDrivers: event.snapshot.topDrivers ? mapSnapshotDrivers(owner, event.snapshot.topDrivers, actorLabels) : undefined, @@ -494,20 +629,21 @@ export async function buildCostInsightsDashboardData(params: { const currentHourStart = floorUtcHour(new Date(asOf)); const endHourExclusive = addHours(currentHourStart, 1); const config = await getCostInsightOwnerConfig(params.database, params.owner); - const [currentHourSpend, rolling24HourSpend, anomalyPolicy, dashboardState, topDrivers, events] = - await Promise.all([ - getOwnerCurrentHourSpend(params.database, params.owner), - getOwnerRolling24HourSpendExact(params.database, { owner: params.owner, asOf }), - getCostInsightAnomalyPolicy(params.database, params.owner, currentHourStart), - getCostInsightDashboardState(params.database, params.owner), - getOwnerTopSpendDrivers(params.database, { - owner: params.owner, - startHour: addHours(currentHourStart, -24), - endHourExclusive, - limit: 5, - }), - listCostInsightEvents(params.database, params.owner, { limit: 5 }), - ]); + const [ + currentHourSpend, + rolling24HourSpend, + anomalyPolicy, + dashboardState, + topDriversByRange, + events, + ] = await Promise.all([ + getOwnerCurrentHourSpend(params.database, params.owner), + getOwnerRolling24HourSpendExact(params.database, { owner: params.owner, asOf }), + getCostInsightAnomalyPolicy(params.database, params.owner, currentHourStart), + getCostInsightDashboardState(params.database, params.owner), + loadTopDriversByRange(params.database, params.owner, endHourExclusive), + listCostInsightEvents(params.database, params.owner, { limit: 5 }), + ]); const [ evidence24h, evidence7d, @@ -521,17 +657,31 @@ export async function buildCostInsightsDashboardData(params: { loadRangeEvidence(params.database, params.owner, '7d', endHourExclusive), loadRangeEvidence(params.database, params.owner, '30d', endHourExclusive), loadRangeEvidence(params.database, params.owner, '90d', endHourExclusive), - loadActorLabels( - params.database, - topDrivers.map(driver => driver.actorUserId) - ), + loadActorLabels(params.database, [ + ...Object.values(topDriversByRange).flatMap(drivers => + drivers.map(driver => driver.actorUserId) + ), + ...dashboardState.events.flatMap(event => + (event.snapshot.topDrivers ?? []) + .map(driver => driver.actorUserId) + .filter((actorUserId): actorUserId is string => Boolean(actorUserId)) + ), + ]), (config?.cost_suggestions_enabled ?? true) ? listActiveCostInsightSuggestions(params.database, params.owner) : [], mapEvents(params.database, params.owner, events), ]); - const alerts = formatActiveCostInsightAlerts(dashboardState); + const evidenceThisHour: SpendEvidencePoint[] = [ + { + label: formatHourLabel(currentHourStart, '1h'), + periodStart: currentHourStart, + variableUsd: microdollarsToUsd(currentHourSpend.variableMicrodollars), + scheduledUsd: microdollarsToUsd(currentHourSpend.scheduledMicrodollars), + }, + ]; + const alerts = formatActiveCostInsightAlerts(dashboardState, params.owner, actorLabels); return { enabled: config?.spend_alerts_enabled ?? false, owner: params.uiOwner, @@ -547,22 +697,22 @@ export async function buildCostInsightsDashboardData(params: { }), evidence: evidence7d, evidenceByRange: { + '1h': evidenceThisHour, '24h': evidence24h, '7d': evidence7d, '30d': evidence30d, '90d': evidence90d, }, - drivers: mapDrivers(params.owner, topDrivers, actorLabels), + driversByRange: { + '1h': mapDrivers(params.owner, topDriversByRange['1h'], actorLabels), + '24h': mapDrivers(params.owner, topDriversByRange['24h'], actorLabels), + '7d': mapDrivers(params.owner, topDriversByRange['7d'], actorLabels), + '30d': mapDrivers(params.owner, topDriversByRange['30d'], actorLabels), + '90d': mapDrivers(params.owner, topDriversByRange['90d'], actorLabels), + }, alerts, suggestions: formatActiveCostInsightSuggestions(activeSuggestions), - lastEvaluatedLabel: dashboardState.state?.lastEvaluatedAt - ? `Last evaluated ${new Intl.DateTimeFormat('en-US', { - hour: 'numeric', - minute: '2-digit', - timeZone: 'UTC', - timeZoneName: 'short', - }).format(new Date(dashboardState.state.lastEvaluatedAt))}` - : 'Not evaluated yet', + lastEvaluatedAt: dashboardState.state?.lastEvaluatedAt ?? null, baselineMode: anomalyPolicy.mode, eventPreview, }; @@ -578,8 +728,11 @@ export async function buildCostInsightsSettingsData(params: { return { owner: params.uiOwner, enabled: config.spend_alerts_enabled, + anomalyAlertsEnabled: config.anomaly_alerts_enabled, suggestionsEnabled: config.cost_suggestions_enabled, thresholdUsd: formatSpendThresholdUsd(config.spend_threshold_microdollars), + threshold7DayUsd: formatSpendThresholdUsd(config.spend_7_day_threshold_microdollars), + threshold30DayUsd: formatSpendThresholdUsd(config.spend_30_day_threshold_microdollars), saveState: 'saved', readOnly: params.readOnly, }; diff --git a/apps/web/src/lib/cost-insights/repository.ts b/apps/web/src/lib/cost-insights/repository.ts index 3494457464..2baf0dedf5 100644 --- a/apps/web/src/lib/cost-insights/repository.ts +++ b/apps/web/src/lib/cost-insights/repository.ts @@ -42,10 +42,18 @@ function eventTypesForFilter(filter: CostInsightEventFilter): CostInsightEventTy return filter === 'all' ? null : eventTypesByFilter[filter]; } +export type CostInsightThresholdAlertKind = Extract< + CostInsightAlertKind, + 'threshold' | 'threshold_7d' | 'threshold_30d' +>; + export type CostInsightConfigPatch = { spendAlertsEnabled?: boolean; + anomalyAlertsEnabled?: boolean; costSuggestionsEnabled?: boolean; spendThresholdMicrodollars?: number | null; + spend7DayThresholdMicrodollars?: number | null; + spend30DayThresholdMicrodollars?: number | null; }; export type CostInsightEventInput = { @@ -122,11 +130,20 @@ export async function updateCostInsightOwnerConfig( .update(cost_insight_owner_configs) .set({ spend_alerts_enabled: nextSpendAlertsEnabled, + anomaly_alerts_enabled: patch.anomalyAlertsEnabled ?? previous.anomaly_alerts_enabled, cost_suggestions_enabled: patch.costSuggestionsEnabled ?? previous.cost_suggestions_enabled, spend_threshold_microdollars: patch.spendThresholdMicrodollars === undefined ? previous.spend_threshold_microdollars : patch.spendThresholdMicrodollars, + spend_7_day_threshold_microdollars: + patch.spend7DayThresholdMicrodollars === undefined + ? previous.spend_7_day_threshold_microdollars + : patch.spend7DayThresholdMicrodollars, + spend_30_day_threshold_microdollars: + patch.spend30DayThresholdMicrodollars === undefined + ? previous.spend_30_day_threshold_microdollars + : patch.spend30DayThresholdMicrodollars, spend_alerts_enabled_at: nextSpendAlertsEnabled ? (previous.spend_alerts_enabled_at ?? sql`now()`) : null, @@ -173,6 +190,33 @@ export async function clearCostInsightAlertState( active_threshold_event_id: null, threshold_crossing_started_at: null, threshold_reviewed_at: null, + threshold_recovered_at: null, + rolling_7_day_threshold_crossing_active: false, + active_rolling_7_day_threshold_event_id: null, + rolling_7_day_threshold_crossing_started_at: null, + rolling_7_day_threshold_reviewed_at: null, + rolling_7_day_threshold_recovered_at: null, + rolling_30_day_threshold_crossing_active: false, + active_rolling_30_day_threshold_event_id: null, + rolling_30_day_threshold_crossing_started_at: null, + rolling_30_day_threshold_reviewed_at: null, + rolling_30_day_threshold_recovered_at: null, + updated_at: sql`now()`, + }) + .where(eq(cost_insight_owner_states.id, state.id)); +} + +export async function clearCostInsightAnomalyEpisode( + database: CostInsightDatabase, + owner: CostInsightSpendOwner +): Promise { + const state = await getOrCreateCostInsightOwnerState(database, owner); + await database + .update(cost_insight_owner_states) + .set({ + active_anomaly_event_id: null, + active_anomaly_hour_start: null, + active_anomaly_reviewed_at: null, updated_at: sql`now()`, }) .where(eq(cost_insight_owner_states.id, state.id)); @@ -249,15 +293,24 @@ export async function listCostInsightNotificationRecipientUserIds( database: CostInsightDatabase, owner: CostInsightSpendOwner ): Promise { - if (owner.type === 'user') return [owner.id]; + if (owner.type === 'user') { + const [admin] = await database + .select({ id: kilocode_users.id }) + .from(kilocode_users) + .where(and(eq(kilocode_users.id, owner.id), eq(kilocode_users.is_admin, true))) + .limit(1); + return admin ? [admin.id] : []; + } const rows = await database .select({ userId: organization_memberships.kilo_user_id }) .from(organization_memberships) + .innerJoin(kilocode_users, eq(kilocode_users.id, organization_memberships.kilo_user_id)) .where( and( eq(organization_memberships.organization_id, owner.id), - inArray(organization_memberships.role, ['owner', 'billing_manager']) + inArray(organization_memberships.role, ['owner', 'billing_manager']), + eq(kilocode_users.is_admin, true) ) ) .orderBy(organization_memberships.kilo_user_id); @@ -350,15 +403,27 @@ export async function getCostInsightDashboardState( activeThresholdEventId: cost_insight_owner_states.active_threshold_event_id, thresholdCrossingActive: cost_insight_owner_states.threshold_crossing_active, thresholdReviewedAt: cost_insight_owner_states.threshold_reviewed_at, + active7DayThresholdEventId: cost_insight_owner_states.active_rolling_7_day_threshold_event_id, + threshold7DayCrossingActive: + cost_insight_owner_states.rolling_7_day_threshold_crossing_active, + threshold7DayReviewedAt: cost_insight_owner_states.rolling_7_day_threshold_reviewed_at, + active30DayThresholdEventId: + cost_insight_owner_states.active_rolling_30_day_threshold_event_id, + threshold30DayCrossingActive: + cost_insight_owner_states.rolling_30_day_threshold_crossing_active, + threshold30DayReviewedAt: cost_insight_owner_states.rolling_30_day_threshold_reviewed_at, lastEvaluatedAt: cost_insight_owner_states.last_evaluated_at, }) .from(cost_insight_owner_states) .where(costInsightOwnerWhere(owner, cost_insight_owner_states)) .limit(1); - const eventIds = [state?.activeAnomalyEventId, state?.activeThresholdEventId].filter( - (id): id is string => Boolean(id) - ); + const eventIds = [ + state?.activeAnomalyEventId, + state?.activeThresholdEventId, + state?.active7DayThresholdEventId, + state?.active30DayThresholdEventId, + ].filter((id): id is string => Boolean(id)); const events = eventIds.length === 0 @@ -389,71 +454,134 @@ export async function markCostInsightAnomalyEpisode( export async function markCostInsightThresholdEpisode( database: CostInsightDatabase, - params: { owner: CostInsightSpendOwner; eventId: string; crossedAt: string } + params: { + owner: CostInsightSpendOwner; + eventId: string; + crossedAt: string; + alertKind: CostInsightThresholdAlertKind; + } ): Promise { const state = await getOrCreateCostInsightOwnerState(database, params.owner); - await database - .update(cost_insight_owner_states) - .set({ + const values = (() => { + if (params.alertKind === 'threshold_7d') { + return { + rolling_7_day_threshold_crossing_active: true, + active_rolling_7_day_threshold_event_id: params.eventId, + rolling_7_day_threshold_crossing_started_at: params.crossedAt, + rolling_7_day_threshold_reviewed_at: null, + rolling_7_day_threshold_recovered_at: null, + updated_at: sql`now()`, + }; + } + if (params.alertKind === 'threshold_30d') { + return { + rolling_30_day_threshold_crossing_active: true, + active_rolling_30_day_threshold_event_id: params.eventId, + rolling_30_day_threshold_crossing_started_at: params.crossedAt, + rolling_30_day_threshold_reviewed_at: null, + rolling_30_day_threshold_recovered_at: null, + updated_at: sql`now()`, + }; + } + return { threshold_crossing_active: true, active_threshold_event_id: params.eventId, threshold_crossing_started_at: params.crossedAt, threshold_reviewed_at: null, threshold_recovered_at: null, updated_at: sql`now()`, - }) + }; + })(); + await database + .update(cost_insight_owner_states) + .set(values) .where(eq(cost_insight_owner_states.id, state.id)); } export async function clearCostInsightThresholdEpisode( database: CostInsightDatabase, owner: CostInsightSpendOwner, - recoveredAt: string | null + recoveredAt: string | null, + alertKind: CostInsightThresholdAlertKind = 'threshold' ): Promise { const state = await getOrCreateCostInsightOwnerState(database, owner); - await database - .update(cost_insight_owner_states) - .set({ + const values = (() => { + if (alertKind === 'threshold_7d') { + return { + rolling_7_day_threshold_crossing_active: false, + active_rolling_7_day_threshold_event_id: null, + rolling_7_day_threshold_crossing_started_at: null, + rolling_7_day_threshold_reviewed_at: null, + rolling_7_day_threshold_recovered_at: recoveredAt, + updated_at: sql`now()`, + }; + } + if (alertKind === 'threshold_30d') { + return { + rolling_30_day_threshold_crossing_active: false, + active_rolling_30_day_threshold_event_id: null, + rolling_30_day_threshold_crossing_started_at: null, + rolling_30_day_threshold_reviewed_at: null, + rolling_30_day_threshold_recovered_at: recoveredAt, + updated_at: sql`now()`, + }; + } + return { threshold_crossing_active: false, active_threshold_event_id: null, threshold_crossing_started_at: null, threshold_reviewed_at: null, threshold_recovered_at: recoveredAt, updated_at: sql`now()`, - }) + }; + })(); + await database + .update(cost_insight_owner_states) + .set(values) .where(eq(cost_insight_owner_states.id, state.id)); } export async function acknowledgeCostInsightAlert( database: CostInsightDatabase, params: { owner: CostInsightSpendOwner; alertKind: CostInsightAlertKind; actorUserId: string } -): Promise { +): Promise { const state = await getOrCreateCostInsightOwnerState(database, params.owner); const now = sql`now()`; - const [acknowledged] = await database - .update(cost_insight_owner_states) - .set( - params.alertKind === 'anomaly' - ? { active_anomaly_reviewed_at: now, updated_at: now } - : { threshold_reviewed_at: now, updated_at: now } - ) - .where( - and( - eq(cost_insight_owner_states.id, state.id), - params.alertKind === 'anomaly' + const reviewValues = + params.alertKind === 'anomaly' + ? { active_anomaly_reviewed_at: now, updated_at: now } + : params.alertKind === 'threshold_7d' + ? { rolling_7_day_threshold_reviewed_at: now, updated_at: now } + : params.alertKind === 'threshold_30d' + ? { rolling_30_day_threshold_reviewed_at: now, updated_at: now } + : { threshold_reviewed_at: now, updated_at: now }; + const activeEpisode = + params.alertKind === 'anomaly' + ? and( + isNotNull(cost_insight_owner_states.active_anomaly_event_id), + isNull(cost_insight_owner_states.active_anomaly_reviewed_at) + ) + : params.alertKind === 'threshold_7d' + ? and( + isNotNull(cost_insight_owner_states.active_rolling_7_day_threshold_event_id), + isNull(cost_insight_owner_states.rolling_7_day_threshold_reviewed_at) + ) + : params.alertKind === 'threshold_30d' ? and( - isNotNull(cost_insight_owner_states.active_anomaly_event_id), - isNull(cost_insight_owner_states.active_anomaly_reviewed_at) + isNotNull(cost_insight_owner_states.active_rolling_30_day_threshold_event_id), + isNull(cost_insight_owner_states.rolling_30_day_threshold_reviewed_at) ) : and( isNotNull(cost_insight_owner_states.active_threshold_event_id), isNull(cost_insight_owner_states.threshold_reviewed_at) - ) - ) - ) + ); + const [acknowledged] = await database + .update(cost_insight_owner_states) + .set(reviewValues) + .where(and(eq(cost_insight_owner_states.id, state.id), activeEpisode)) .returning({ id: cost_insight_owner_states.id }); - if (!acknowledged) return; + if (!acknowledged) return false; await createCostInsightEvent(database, { owner: params.owner, @@ -463,9 +591,14 @@ export async function acknowledgeCostInsightAlert( title: params.alertKind === 'anomaly' ? 'Spend Anomaly Alert reviewed' - : 'Spend Threshold Alert reviewed', + : params.alertKind === 'threshold_7d' + ? '7-day Spend Threshold Alert reviewed' + : params.alertKind === 'threshold_30d' + ? '30-day Spend Threshold Alert reviewed' + : '24-hour Spend Threshold Alert reviewed', description: 'Alert acknowledgment recorded for the current episode.', }); + return true; } export async function upsertCostInsightActiveSuggestion( @@ -529,7 +662,7 @@ export async function listActiveCostInsightSuggestions( export async function dismissCostInsightSuggestion( database: CostInsightDatabase, params: { owner: CostInsightSpendOwner; suggestionId: string; actorUserId: string } -): Promise { +): Promise { const [suggestion] = await database .update(cost_insight_active_suggestions) .set({ @@ -546,7 +679,7 @@ export async function dismissCostInsightSuggestion( ) .returning(); - if (!suggestion) return; + if (!suggestion) return null; await createCostInsightEvent(database, { owner: params.owner, eventType: 'suggestion_dismissed', @@ -565,6 +698,7 @@ export async function dismissCostInsightSuggestion( }, }, }); + return suggestion.suggestion_kind; } export async function hasCurrentCostInsightAccess( @@ -572,15 +706,25 @@ export async function hasCurrentCostInsightAccess( owner: CostInsightSpendOwner, userId: string ): Promise { - if (owner.type === 'user') return owner.id === userId; + if (owner.type === 'user') { + if (owner.id !== userId) return false; + const [admin] = await database + .select({ id: kilocode_users.id }) + .from(kilocode_users) + .where(and(eq(kilocode_users.id, userId), eq(kilocode_users.is_admin, true))) + .limit(1); + return Boolean(admin); + } const [row] = await database .select({ id: organization_memberships.id }) .from(organization_memberships) + .innerJoin(kilocode_users, eq(kilocode_users.id, organization_memberships.kilo_user_id)) .where( and( eq(organization_memberships.organization_id, owner.id), eq(organization_memberships.kilo_user_id, userId), - inArray(organization_memberships.role, ['owner', 'billing_manager']) + inArray(organization_memberships.role, ['owner', 'billing_manager']), + eq(kilocode_users.is_admin, true) ) ) .limit(1); @@ -636,6 +780,14 @@ export async function ownerHasUnreviewedCostInsightAlert( and( isNull(cost_insight_owner_states.threshold_reviewed_at), sql`${cost_insight_owner_states.active_threshold_event_id} IS NOT NULL` + ), + and( + isNull(cost_insight_owner_states.rolling_7_day_threshold_reviewed_at), + sql`${cost_insight_owner_states.active_rolling_7_day_threshold_event_id} IS NOT NULL` + ), + and( + isNull(cost_insight_owner_states.rolling_30_day_threshold_reviewed_at), + sql`${cost_insight_owner_states.active_rolling_30_day_threshold_event_id} IS NOT NULL` ) ) ) @@ -656,6 +808,12 @@ export async function countOpenCostInsightReviewItems( activeAnomalyReviewedAt: cost_insight_owner_states.active_anomaly_reviewed_at, activeThresholdEventId: cost_insight_owner_states.active_threshold_event_id, thresholdReviewedAt: cost_insight_owner_states.threshold_reviewed_at, + active7DayThresholdEventId: + cost_insight_owner_states.active_rolling_7_day_threshold_event_id, + threshold7DayReviewedAt: cost_insight_owner_states.rolling_7_day_threshold_reviewed_at, + active30DayThresholdEventId: + cost_insight_owner_states.active_rolling_30_day_threshold_event_id, + threshold30DayReviewedAt: cost_insight_owner_states.rolling_30_day_threshold_reviewed_at, }) .from(cost_insight_owner_states) .where(costInsightOwnerWhere(owner, cost_insight_owner_states)) @@ -674,7 +832,9 @@ export async function countOpenCostInsightReviewItems( const activeState = state[0]; const alertCount = (activeState?.activeAnomalyEventId && !activeState.activeAnomalyReviewedAt ? 1 : 0) + - (activeState?.activeThresholdEventId && !activeState.thresholdReviewedAt ? 1 : 0); + (activeState?.activeThresholdEventId && !activeState.thresholdReviewedAt ? 1 : 0) + + (activeState?.active7DayThresholdEventId && !activeState.threshold7DayReviewedAt ? 1 : 0) + + (activeState?.active30DayThresholdEventId && !activeState.threshold30DayReviewedAt ? 1 : 0); const suggestionCount = (config?.cost_suggestions_enabled ?? true) ? (suggestions[0]?.value ?? 0) : 0; return alertCount + suggestionCount; diff --git a/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts b/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts index 565eedd0f8..f139a0a9cf 100644 --- a/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts +++ b/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts @@ -13,6 +13,7 @@ import { import { getOwnerHourlySpend, + getOwnerRolling24HourDriverEvidenceExact, getOwnerRolling24HourSpendExact, getOwnerTopSpendDrivers, } from './spend-repository'; @@ -151,6 +152,153 @@ describe('Cost Insights spend repository integration', () => { }); }); + test('returns exact rolling 24-hour canonical driver evidence', async () => { + const userId = await createUser(); + await db.insert(microdollar_usage).values([ + { + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 10_000_000, + input_tokens: 100, + output_tokens: 50, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-01T11:29:59.999Z', + provider: 'anthropic', + model: 'claude-sonnet-4', + requested_model: 'claude-sonnet-4', + inference_provider: 'anthropic', + has_error: false, + abuse_classification: 0, + }, + { + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 20_000_000, + input_tokens: 200, + output_tokens: 100, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-01T11:30:00.000Z', + provider: 'openai', + model: 'gpt-4.1', + requested_model: 'gpt-4.1', + inference_provider: 'openai', + has_error: false, + abuse_classification: 0, + }, + { + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 30_000_000, + input_tokens: 300, + output_tokens: 150, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-02T11:29:59.999Z', + provider: 'google', + model: 'gemini-2.5-pro', + requested_model: 'gemini-2.5-pro', + inference_provider: 'google', + has_error: false, + abuse_classification: 0, + }, + { + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 40_000_000, + input_tokens: 400, + output_tokens: 200, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-02T11:30:00.000Z', + provider: 'openai', + model: 'gpt-5', + requested_model: 'gpt-5', + inference_provider: 'openai', + has_error: false, + abuse_classification: 0, + }, + ]); + + await expect( + getOwnerRolling24HourDriverEvidenceExact(db, { + owner: { type: 'user', id: userId }, + asOf: '2026-06-02T11:30:00.000Z', + }) + ).resolves.toEqual({ + asOf: '2026-06-02T11:30:00.000Z', + windowStart: '2026-06-01T11:30:00.000Z', + variableMicrodollars: 50_000_000, + scheduledMicrodollars: 0, + totalMicrodollars: 50_000_000, + topDrivers: [ + expect.objectContaining({ + category: 'variable', + totalMicrodollars: 30_000_000, + spendRecordCount: 1, + }), + expect.objectContaining({ + category: 'variable', + totalMicrodollars: 20_000_000, + spendRecordCount: 1, + }), + ], + }); + }); + + test('filters top drivers to the requested hour and spend category', async () => { + const userId = await createUser(); + const baseDriver = { + owned_by_user_id: userId, + source: 'ai_gateway' as const, + product_key: 'direct-gateway', + feature_key: 'chat_completions', + model_or_plan_key: 'model', + provider_key: 'provider', + actor_user_id: userId, + spend_record_count: 1, + }; + await db.insert(cost_insight_owner_hour_driver_buckets).values([ + { + ...baseDriver, + hour_start: '2026-06-01T00:00:00.000Z', + spend_category: 'variable', + driver_key: 'b'.repeat(64), + total_microdollars: 30, + }, + { + ...baseDriver, + hour_start: '2026-06-01T00:00:00.000Z', + spend_category: 'scheduled', + driver_key: 'c'.repeat(64), + total_microdollars: 90, + }, + { + ...baseDriver, + hour_start: '2026-05-31T23:00:00.000Z', + spend_category: 'variable', + driver_key: 'd'.repeat(64), + total_microdollars: 120, + }, + ]); + + await expect( + getOwnerTopSpendDrivers(db, { + owner: { type: 'user', id: userId }, + startHour: '2026-06-01T00:00:00.000Z', + endHourExclusive: '2026-06-01T01:00:00.000Z', + category: 'variable', + }) + ).resolves.toEqual([ + expect.objectContaining({ + category: 'variable', + totalMicrodollars: 30, + spendRecordCount: 1, + }), + ]); + }); + test('combines rollup interior with canonical raw boundary fragments exactly once', async () => { const userId = await createUser(); await initializeCoverage(); diff --git a/apps/web/src/lib/cost-insights/spend-repository.test.ts b/apps/web/src/lib/cost-insights/spend-repository.test.ts index 0ffeeade0a..2bca1779c6 100644 --- a/apps/web/src/lib/cost-insights/spend-repository.test.ts +++ b/apps/web/src/lib/cost-insights/spend-repository.test.ts @@ -4,6 +4,7 @@ import { getOwnerCurrentHourSpend, getOwnerHourlySpend, getRolling24HourFragments, + getRollingWindowFragments, } from './spend-repository'; const owner = { type: 'user', id: 'user-1' } as const; @@ -28,6 +29,28 @@ describe('Cost Insights spend repository', () => { }); }); + test('splits an exact rolling 30-day window into raw boundaries and rollup interior', () => { + expect(getRollingWindowFragments('2026-06-02T12:30:00.000Z', 30 * 24)).toEqual({ + asOf: '2026-06-02T12:30:00.000Z', + windowStart: '2026-05-03T12:30:00.000Z', + oldestBoundaryEnd: '2026-05-03T13:00:00.000Z', + interiorStart: '2026-05-03T13:00:00.000Z', + interiorEnd: '2026-06-02T12:00:00.000Z', + currentBoundaryStart: '2026-06-02T12:00:00.000Z', + }); + }); + + test('splits an exact rolling 7-day window into raw boundaries and rollup interior', () => { + expect(getRollingWindowFragments('2026-06-02T12:30:00.000Z', 7 * 24)).toEqual({ + asOf: '2026-06-02T12:30:00.000Z', + windowStart: '2026-05-26T12:30:00.000Z', + oldestBoundaryEnd: '2026-05-26T13:00:00.000Z', + interiorStart: '2026-05-26T13:00:00.000Z', + interiorEnd: '2026-06-02T12:00:00.000Z', + currentBoundaryStart: '2026-06-02T12:00:00.000Z', + }); + }); + test('skips both raw fragments on exact UTC-hour boundaries', () => { const fragments = getRolling24HourFragments('2026-06-02T12:00:00.000Z'); expect(fragments.windowStart).toBe(fragments.oldestBoundaryEnd); diff --git a/apps/web/src/lib/cost-insights/spend-repository.ts b/apps/web/src/lib/cost-insights/spend-repository.ts index 2b7a824243..66da5accb6 100644 --- a/apps/web/src/lib/cost-insights/spend-repository.ts +++ b/apps/web/src/lib/cost-insights/spend-repository.ts @@ -12,6 +12,7 @@ import type { db } from '@/lib/drizzle'; import { COST_INSIGHT_ROLLUP_VERSION, getCanonicalOwnerSpendTotals, + loadCanonicalCostInsightAggregation, parseSafeDatabaseInteger, requireUtcHour, requireUtcTimestamp, @@ -64,7 +65,7 @@ export type CostInsightRollupCoverage = { isFullyCovered: boolean; }; -export type OwnerRolling24HourSpendExact = { +export type OwnerRollingSpendExact = { asOf: string; windowStart: string; variableMicrodollars: number | null; @@ -73,7 +74,16 @@ export type OwnerRolling24HourSpendExact = { isComplete: boolean; }; -export type Rolling24HourFragments = { +export type OwnerRollingDriverEvidenceExact = { + asOf: string; + windowStart: string; + variableMicrodollars: number; + scheduledMicrodollars: number; + totalMicrodollars: number; + topDrivers: OwnerTopSpendDriver[]; +}; + +export type RollingWindowFragments = { asOf: string; windowStart: string; oldestBoundaryEnd: string; @@ -82,6 +92,10 @@ export type Rolling24HourFragments = { currentBoundaryStart: string; }; +export type OwnerRolling24HourSpendExact = OwnerRollingSpendExact; +export type OwnerRolling24HourDriverEvidenceExact = OwnerRollingDriverEvidenceExact; +export type Rolling24HourFragments = RollingWindowFragments; + type DenseHourlySpendRow = { hour_start: string | Date; variable_microdollars: string | number | bigint | null; @@ -194,10 +208,16 @@ function ceilUtcHour(timestamp: number): number { return Math.ceil(timestamp / HOUR_MS) * HOUR_MS; } -export function getRolling24HourFragments(asOfInput: string): Rolling24HourFragments { +export function getRollingWindowFragments( + asOfInput: string, + windowHours: number +): RollingWindowFragments { + if (!Number.isSafeInteger(windowHours) || windowHours <= 0 || windowHours > 24 * 90) { + throw new Error('Cost Insights rolling window must contain between 1 and 2160 hours.'); + } const asOf = requireUtcTimestamp(asOfInput, 'asOf'); const asOfTimestamp = Date.parse(asOf); - const windowStartTimestamp = asOfTimestamp - 24 * HOUR_MS; + const windowStartTimestamp = asOfTimestamp - windowHours * HOUR_MS; const oldestBoundaryEndTimestamp = ceilUtcHour(windowStartTimestamp); const currentBoundaryStartTimestamp = floorUtcHour(asOfTimestamp); return { @@ -210,6 +230,10 @@ export function getRolling24HourFragments(asOfInput: string): Rolling24HourFragm }; } +export function getRolling24HourFragments(asOfInput: string): Rolling24HourFragments { + return getRollingWindowFragments(asOfInput, 24); +} + export async function getOwnerHourlySpend( executor: CostInsightQueryExecutor, params: { @@ -586,10 +610,117 @@ async function getInteriorRollupTotals( return { variableMicrodollars, scheduledMicrodollars }; } -export async function getOwnerRolling24HourSpendExact( +function compareTopSpendDrivers(left: OwnerTopSpendDriver, right: OwnerTopSpendDriver): number { + if (left.totalMicrodollars !== right.totalMicrodollars) { + return right.totalMicrodollars - left.totalMicrodollars; + } + const leftKey = [ + left.category, + left.source, + left.productKey, + left.featureKey, + left.modelOrPlanKey, + left.providerKey, + left.actorUserId, + ].join('\u0000'); + const rightKey = [ + right.category, + right.source, + right.productKey, + right.featureKey, + right.modelOrPlanKey, + right.providerKey, + right.actorUserId, + ].join('\u0000'); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; +} + +export async function getOwnerRollingDriverEvidenceExact( + primaryDatabase: ExactRollingDatabase, + params: { owner: CostInsightSpendOwner; windowHours: number; asOf?: string } +): Promise { + const requestedAsOf = + params.asOf === undefined ? undefined : requireUtcTimestamp(params.asOf, 'asOf'); + + return primaryDatabase.transaction( + async transaction => { + const asOfResult = await transaction.execute(sql` + SELECT COALESCE(${requestedAsOf ?? null}::timestamptz, CURRENT_TIMESTAMP) AS value + `); + const asOfRow = asOfResult.rows[0]; + if (!asOfRow) { + throw new Error('Cost Insights exact driver query could not establish an as-of value.'); + } + const asOf = normalizeDatabaseTimestamp(asOfRow.value, 'as_of'); + const windowStart = getRollingWindowFragments(asOf, params.windowHours).windowStart; + const aggregation = await loadCanonicalCostInsightAggregation(transaction, { + owner: params.owner, + startInclusive: windowStart, + endExclusive: asOf, + }); + const variableMicrodollars = aggregation.totals + .filter(total => total.category === 'variable') + .reduce( + (sum, total) => sumSafe(sum, total.totalMicrodollars, 'exact driver variable total'), + 0 + ); + const scheduledMicrodollars = aggregation.totals + .filter(total => total.category === 'scheduled') + .reduce( + (sum, total) => sumSafe(sum, total.totalMicrodollars, 'exact driver scheduled total'), + 0 + ); + const topDrivers = aggregation.drivers + .map(driver => ({ + category: driver.category, + source: driver.source, + productKey: driver.productKey, + featureKey: driver.featureKey, + modelOrPlanKey: driver.modelOrPlanKey, + providerKey: driver.providerKey, + actorUserId: driver.actorUserId, + totalMicrodollars: driver.totalMicrodollars, + spendRecordCount: driver.spendRecordCount, + })) + .sort(compareTopSpendDrivers) + .slice(0, COST_INSIGHT_MAX_TOP_DRIVERS); + + return { + asOf, + windowStart, + variableMicrodollars, + scheduledMicrodollars, + totalMicrodollars: sumSafe( + variableMicrodollars, + scheduledMicrodollars, + 'exact driver total microdollars' + ), + topDrivers, + }; + }, + { isolationLevel: 'repeatable read', accessMode: 'read only' } + ); +} + +export async function getOwnerRolling24HourDriverEvidenceExact( primaryDatabase: ExactRollingDatabase, params: { owner: CostInsightSpendOwner; asOf?: string } -): Promise { +): Promise { + return await getOwnerRollingDriverEvidenceExact(primaryDatabase, { + ...params, + windowHours: 24, + }); +} + +export async function getOwnerRollingSpendExact( + primaryDatabase: ExactRollingDatabase, + params: { + owner: CostInsightSpendOwner; + windowHours: number; + asOf?: string; + fallbackToCanonical?: boolean; + } +): Promise { const requestedAsOf = params.asOf === undefined ? undefined : requireUtcTimestamp(params.asOf, 'asOf'); @@ -602,8 +733,9 @@ export async function getOwnerRolling24HourSpendExact( if (!asOfRow) { throw new Error('Cost Insights exact rolling query could not establish an as-of value.'); } - const fragments = getRolling24HourFragments( - normalizeDatabaseTimestamp(asOfRow.value, 'as_of') + const fragments = getRollingWindowFragments( + normalizeDatabaseTimestamp(asOfRow.value, 'as_of'), + params.windowHours ); const { asOf, @@ -622,6 +754,25 @@ export async function getOwnerRolling24HourSpendExact( endHourExclusive: interiorEnd, }); if (coverage && !coverage.isFullyCovered) { + if (params.fallbackToCanonical) { + const canonical = await getCanonicalOwnerSpendTotals(transaction, { + owner: params.owner, + startInclusive: windowStart, + endExclusive: asOf, + }); + return { + asOf, + windowStart, + variableMicrodollars: canonical.variableMicrodollars, + scheduledMicrodollars: canonical.scheduledMicrodollars, + totalMicrodollars: sumSafe( + canonical.variableMicrodollars, + canonical.scheduledMicrodollars, + 'canonical rolling total microdollars' + ), + isComplete: true, + }; + } return { asOf, windowStart, @@ -694,3 +845,13 @@ export async function getOwnerRolling24HourSpendExact( { isolationLevel: 'repeatable read', accessMode: 'read only' } ); } + +export async function getOwnerRolling24HourSpendExact( + primaryDatabase: ExactRollingDatabase, + params: { owner: CostInsightSpendOwner; asOf?: string } +): Promise { + return await getOwnerRollingSpendExact(primaryDatabase, { + ...params, + windowHours: 24, + }); +} diff --git a/apps/web/src/lib/cost-insights/tracking.ts b/apps/web/src/lib/cost-insights/tracking.ts new file mode 100644 index 0000000000..459ed946fc --- /dev/null +++ b/apps/web/src/lib/cost-insights/tracking.ts @@ -0,0 +1,110 @@ +import * as z from 'zod'; + +const SpendRangeSchema = z.enum(['1h', '24h', '7d', '30d', '90d']); +const AlertKindSchema = z.enum(['anomaly', 'threshold', 'threshold_7d', 'threshold_30d']); +const ActivityFilterSchema = z.enum(['all', 'alerts', 'suggestions', 'reviews', 'settings']); + +const DashboardViewedSchema = z + .object({ + interaction: z.literal('dashboard_viewed'), + spendAlertsEnabled: z.boolean(), + hasActiveAlert: z.boolean(), + hasActiveSuggestion: z.boolean(), + }) + .strict(); +const SettingsViewedSchema = z + .object({ + interaction: z.literal('settings_viewed'), + spendAlertsEnabled: z.boolean(), + costSuggestionsEnabled: z.boolean(), + threshold24hConfigured: z.boolean(), + threshold7dConfigured: z.boolean(), + threshold30dConfigured: z.boolean(), + readOnly: z.boolean(), + }) + .strict(); +const ActivityViewedSchema = z.object({ interaction: z.literal('activity_viewed') }).strict(); +const AskKiloViewedSchema = z.object({ interaction: z.literal('ask_kilo_viewed') }).strict(); +const SpendRangeSelectedSchema = z + .object({ + interaction: z.literal('spend_range_selected'), + range: SpendRangeSchema, + }) + .strict(); +const AlertDriversExpandedSchema = z + .object({ + interaction: z.literal('alert_drivers_expanded'), + alertKind: AlertKindSchema, + }) + .strict(); +const SetupAlertsClickedSchema = z + .object({ interaction: z.literal('setup_alerts_clicked') }) + .strict(); +const AlertSettingsClickedSchema = z + .object({ + interaction: z.literal('alert_settings_clicked'), + action: z.enum(['manage_threshold', 'disable_alerts']), + }) + .strict(); +const ActivityFilterSelectedSchema = z + .object({ + interaction: z.literal('activity_filter_selected'), + filter: ActivityFilterSchema, + }) + .strict(); +const ActivityPageSelectedSchema = z + .object({ + interaction: z.literal('activity_page_selected'), + direction: z.enum(['next', 'previous']), + }) + .strict(); +const AskKiloQuestionSubmittedSchema = z + .object({ + interaction: z.literal('ask_kilo_question_submitted'), + source: z.enum(['dashboard', 'follow_up']), + experience: z.literal('ui_only'), + }) + .strict(); + +export const CostInsightsUiInteractionSchema = z.discriminatedUnion('interaction', [ + DashboardViewedSchema, + SettingsViewedSchema, + ActivityViewedSchema, + AskKiloViewedSchema, + SpendRangeSelectedSchema, + AlertDriversExpandedSchema, + SetupAlertsClickedSchema, + AlertSettingsClickedSchema, + ActivityFilterSelectedSchema, + ActivityPageSelectedSchema, + AskKiloQuestionSubmittedSchema, +]); + +const OrganizationIdField = { organizationId: z.uuid() }; + +export const OrganizationCostInsightsUiInteractionSchema = z.discriminatedUnion('interaction', [ + DashboardViewedSchema.extend(OrganizationIdField), + SettingsViewedSchema.extend(OrganizationIdField), + ActivityViewedSchema.extend(OrganizationIdField), + AskKiloViewedSchema.extend(OrganizationIdField), + SpendRangeSelectedSchema.extend(OrganizationIdField), + AlertDriversExpandedSchema.extend(OrganizationIdField), + SetupAlertsClickedSchema.extend(OrganizationIdField), + AlertSettingsClickedSchema.extend(OrganizationIdField), + ActivityFilterSelectedSchema.extend(OrganizationIdField), + ActivityPageSelectedSchema.extend(OrganizationIdField), + AskKiloQuestionSubmittedSchema.extend(OrganizationIdField), +]); + +export const CostInsightsSuggestionCtaSchema = z + .object({ + suggestionKind: z.enum(['coding_plan', 'kilo_pass']), + }) + .strict(); + +export const OrganizationCostInsightsSuggestionCtaSchema = CostInsightsSuggestionCtaSchema.extend({ + organizationId: z.uuid(), +}); + +export type CostInsightsUiInteraction = z.infer; +export type CostInsightsSuggestionCta = z.infer; diff --git a/apps/web/src/routers/cost-insights-router.test.ts b/apps/web/src/routers/cost-insights-router.test.ts index 2169fd86c8..646541eae4 100644 --- a/apps/web/src/routers/cost-insights-router.test.ts +++ b/apps/web/src/routers/cost-insights-router.test.ts @@ -1,3 +1,4 @@ +import { jest } from '@jest/globals'; import { cost_insight_active_suggestions, cost_insight_events, @@ -7,18 +8,115 @@ import { import { eq } from 'drizzle-orm'; import { db } from '@/lib/drizzle'; -import { createCallerForUser } from '@/routers/test-utils'; +import type { createCallerForUser as CreateCallerForUser } from '@/routers/test-utils'; import { insertTestUser } from '@/tests/helpers/user.helper'; +jest.mock('@/lib/cost-insights/posthog-tracking', () => ({ + trackCostInsightsAlertAction: jest.fn(), + trackCostInsightsSettingsSaved: jest.fn(), + trackCostInsightsSuggestionAction: jest.fn(), + trackCostInsightsUiInteraction: jest.fn(), +})); + +const trackingMock: { + trackCostInsightsAlertAction: jest.Mock; + trackCostInsightsSettingsSaved: jest.Mock; + trackCostInsightsSuggestionAction: jest.Mock; + trackCostInsightsUiInteraction: jest.Mock; +} = jest.requireMock('@/lib/cost-insights/posthog-tracking'); + +let createCallerForUser: typeof CreateCallerForUser; + +beforeAll(async () => { + ({ createCallerForUser } = await import('@/routers/test-utils')); +}); + describe('Cost Insights router', () => { - it('counts open alerts and suggestions for sidebar review badge', async () => { + beforeEach(() => { + trackingMock.trackCostInsightsAlertAction.mockClear(); + trackingMock.trackCostInsightsSettingsSaved.mockClear(); + trackingMock.trackCostInsightsSuggestionAction.mockClear(); + trackingMock.trackCostInsightsUiInteraction.mockClear(); + }); + + it('rejects every Cost Insights procedure for non-admin users', async () => { const user = await insertTestUser(); + const caller = await createCallerForUser(user.id); + const calls = [ + () => + caller.costInsights.trackUiInteraction({ + interaction: 'spend_range_selected' as const, + range: '24h' as const, + }), + () => + caller.costInsights.trackSuggestionCta({ + suggestionKind: 'kilo_pass' as const, + }), + () => caller.costInsights.getDashboard(), + () => caller.costInsights.getSettings(), + () => caller.costInsights.listEvents({ filter: 'all', page: 1, pageSize: 10 }), + () => caller.costInsights.getAttentionState(), + () => + caller.costInsights.updateSettings({ + spendAlertsEnabled: false, + anomalyAlertsEnabled: true, + costSuggestionsEnabled: true, + spendThresholdUsd: null, + spend7DayThresholdUsd: null, + spend30DayThresholdUsd: null, + }), + () => caller.costInsights.acknowledgeAlert({ alertKind: 'anomaly' }), + () => caller.costInsights.disableThreshold(), + () => caller.costInsights.dismissSuggestion({ suggestionId: crypto.randomUUID() }), + ]; + + for (const call of calls) { + await expect(call()).rejects.toMatchObject({ + code: 'FORBIDDEN', + message: 'Admin access required', + }); + } + }); + + it('tracks allowlisted UI interactions with authenticated personal context', async () => { + const user = await insertTestUser({ is_admin: true }); + const caller = await createCallerForUser(user.id); + + await expect( + caller.costInsights.trackUiInteraction({ + interaction: 'spend_range_selected', + range: '90d', + }) + ).resolves.toEqual({ success: true }); + expect(trackingMock.trackCostInsightsUiInteraction).toHaveBeenCalledWith( + { + distinctId: user.id, + userId: user.id, + ownerType: 'personal', + authorizedRole: 'personal', + }, + { interaction: 'spend_range_selected', range: '90d' } + ); + + await expect( + caller.costInsights.trackUiInteraction({ + interaction: 'ask_kilo_question_submitted', + source: 'follow_up', + experience: 'ui_only', + question: 'private question', + } as never) + ).rejects.toThrow(); + expect(trackingMock.trackCostInsightsUiInteraction).toHaveBeenCalledTimes(1); + }); + + it('counts open alerts and suggestions for sidebar review badge', async () => { + const user = await insertTestUser({ is_admin: true }); await db.insert(cost_insight_owner_configs).values({ owned_by_user_id: user.id, spend_alerts_enabled: true, cost_suggestions_enabled: true, }); - const [anomalyEvent, thresholdEvent] = await db + const [anomalyEvent, thresholdEvent, threshold7DayEvent, threshold30DayEvent] = await db .insert(cost_insight_events) .values([ { @@ -32,12 +130,26 @@ describe('Cost Insights router', () => { owned_by_user_id: user.id, event_type: 'threshold_crossed', alert_kind: 'threshold', - title: 'Spend Threshold Alert', - description: 'Rolling spend crossed threshold.', + title: '24-hour Spend Threshold Alert', + description: 'Rolling 24-hour spend crossed threshold.', + }, + { + owned_by_user_id: user.id, + event_type: 'threshold_crossed', + alert_kind: 'threshold_7d', + title: '7-day Spend Threshold Alert', + description: 'Rolling 7-day spend crossed threshold.', + }, + { + owned_by_user_id: user.id, + event_type: 'threshold_crossed', + alert_kind: 'threshold_30d', + title: '30-day Spend Threshold Alert', + description: 'Rolling 30-day spend crossed threshold.', }, ]) .returning({ id: cost_insight_events.id }); - if (!anomalyEvent || !thresholdEvent) { + if (!anomalyEvent || !thresholdEvent || !threshold7DayEvent || !threshold30DayEvent) { throw new Error('Cost Insights alert event fixture insert failed.'); } await db.insert(cost_insight_owner_states).values({ @@ -46,6 +158,12 @@ describe('Cost Insights router', () => { active_threshold_event_id: thresholdEvent.id, threshold_crossing_active: true, threshold_crossing_started_at: '2026-06-25T19:00:00.000Z', + active_rolling_7_day_threshold_event_id: threshold7DayEvent.id, + rolling_7_day_threshold_crossing_active: true, + rolling_7_day_threshold_crossing_started_at: '2026-06-25T19:00:00.000Z', + active_rolling_30_day_threshold_event_id: threshold30DayEvent.id, + rolling_30_day_threshold_crossing_active: true, + rolling_30_day_threshold_crossing_started_at: '2026-06-25T19:00:00.000Z', }); await db.insert(cost_insight_active_suggestions).values({ owned_by_user_id: user.id, @@ -65,7 +183,7 @@ describe('Cost Insights router', () => { const caller = await createCallerForUser(user.id); await expect(caller.costInsights.getAttentionState()).resolves.toEqual({ attention: 'alert', - reviewItemCount: 3, + reviewItemCount: 5, }); await db @@ -75,12 +193,102 @@ describe('Cost Insights router', () => { await expect(caller.costInsights.getAttentionState()).resolves.toEqual({ attention: 'alert', - reviewItemCount: 2, + reviewItemCount: 4, + }); + }); + + it('defaults anomaly alerts on and saves all spend thresholds as sub-options', async () => { + const user = await insertTestUser({ is_admin: true }); + const caller = await createCallerForUser(user.id); + + await expect(caller.costInsights.getSettings()).resolves.toMatchObject({ + enabled: false, + anomalyAlertsEnabled: true, + thresholdUsd: '', + threshold7DayUsd: '', + threshold30DayUsd: '', + }); + + const [anomalyEvent] = await db + .insert(cost_insight_events) + .values({ + owned_by_user_id: user.id, + event_type: 'anomaly_alert', + alert_kind: 'anomaly', + title: 'Spend Anomaly Alert', + description: 'Usage-based spend is high.', + }) + .returning({ id: cost_insight_events.id }); + if (!anomalyEvent) throw new Error('Cost Insights anomaly event fixture insert failed.'); + await db.insert(cost_insight_owner_states).values({ + owned_by_user_id: user.id, + active_anomaly_event_id: anomalyEvent.id, + active_anomaly_hour_start: '2026-06-25T19:00:00.000Z', + }); + + await expect( + caller.costInsights.updateSettings({ + spendAlertsEnabled: false, + anomalyAlertsEnabled: false, + costSuggestionsEnabled: true, + spendThresholdUsd: '150.00', + spend7DayThresholdUsd: '500.00', + spend30DayThresholdUsd: '1000.00', + }) + ).resolves.toEqual({ success: true }); + expect(trackingMock.trackCostInsightsSettingsSaved).toHaveBeenCalledWith({ + distinctId: user.id, + userId: user.id, + ownerType: 'personal', + authorizedRole: 'personal', + spendAlertsTransition: 'unchanged', + anomalyAlertsTransition: 'disabled', + costSuggestionsTransition: 'unchanged', + threshold24hTransition: 'added', + threshold7dTransition: 'added', + threshold30dTransition: 'added', + spendAlertsEnabled: false, + anomalyAlertsEnabled: false, + costSuggestionsEnabled: true, + threshold24hConfigured: true, + threshold7dConfigured: true, + threshold30dConfigured: true, + }); + + await caller.costInsights.updateSettings({ + spendAlertsEnabled: false, + anomalyAlertsEnabled: false, + costSuggestionsEnabled: true, + spendThresholdUsd: '150.00', + spend7DayThresholdUsd: '500.00', + spend30DayThresholdUsd: '1000.00', + }); + expect(trackingMock.trackCostInsightsSettingsSaved).toHaveBeenCalledTimes(1); + + const [config] = await db + .select() + .from(cost_insight_owner_configs) + .where(eq(cost_insight_owner_configs.owned_by_user_id, user.id)); + const [state] = await db + .select() + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_user_id, user.id)); + + expect(config).toMatchObject({ + anomaly_alerts_enabled: false, + spend_threshold_microdollars: 150_000_000, + spend_7_day_threshold_microdollars: 500_000_000, + spend_30_day_threshold_microdollars: 1_000_000_000, + }); + expect(state).toMatchObject({ + active_anomaly_event_id: null, + active_anomaly_hour_start: null, + active_anomaly_reviewed_at: null, }); }); it('turns off the threshold and clears the active threshold episode', async () => { - const user = await insertTestUser(); + const user = await insertTestUser({ is_admin: true }); await db.insert(cost_insight_owner_configs).values({ owned_by_user_id: user.id, spend_alerts_enabled: true, @@ -134,8 +342,73 @@ describe('Cost Insights router', () => { }); }); + it('tracks accepted alert reviews and suggestion dismissals only once', async () => { + const user = await insertTestUser({ is_admin: true }); + const [alertEvent] = await db + .insert(cost_insight_events) + .values({ + owned_by_user_id: user.id, + event_type: 'threshold_crossed', + alert_kind: 'threshold_7d', + title: '7-day Spend Threshold Alert', + description: 'Rolling 7-day spend crossed threshold.', + }) + .returning({ id: cost_insight_events.id }); + if (!alertEvent) throw new Error('Cost Insights alert fixture insert failed.'); + await db.insert(cost_insight_owner_states).values({ + owned_by_user_id: user.id, + active_rolling_7_day_threshold_event_id: alertEvent.id, + rolling_7_day_threshold_crossing_active: true, + rolling_7_day_threshold_crossing_started_at: '2026-06-25T19:00:00.000Z', + }); + const [suggestion] = await db + .insert(cost_insight_active_suggestions) + .values({ + owned_by_user_id: user.id, + suggestion_kind: 'coding_plan', + suggestion_key: 'b'.repeat(64), + title: 'Compare Coding Plans', + description: 'A Coding Plan may improve cost efficiency.', + cta_label: 'Compare plans', + cta_href: '/pricing', + evidence_window_start: '2026-06-18T19:00:00.000Z', + evidence_window_end: '2026-06-25T19:00:00.000Z', + observed_microdollars: 125_000_000, + benefit_label: 'Included usage', + benefit_detail: 'Current plan terms apply', + }) + .returning({ id: cost_insight_active_suggestions.id }); + if (!suggestion) throw new Error('Cost Insights suggestion fixture insert failed.'); + + const caller = await createCallerForUser(user.id); + await caller.costInsights.acknowledgeAlert({ alertKind: 'threshold_7d' }); + await caller.costInsights.acknowledgeAlert({ alertKind: 'threshold_7d' }); + expect(trackingMock.trackCostInsightsAlertAction).toHaveBeenCalledTimes(1); + expect(trackingMock.trackCostInsightsAlertAction).toHaveBeenCalledWith({ + distinctId: user.id, + userId: user.id, + ownerType: 'personal', + authorizedRole: 'personal', + action: 'acknowledge', + alertKind: 'threshold_7d', + }); + + await caller.costInsights.dismissSuggestion({ suggestionId: suggestion.id }); + await caller.costInsights.dismissSuggestion({ suggestionId: suggestion.id }); + expect(trackingMock.trackCostInsightsSuggestionAction).toHaveBeenCalledTimes(1); + expect(trackingMock.trackCostInsightsSuggestionAction).toHaveBeenCalledWith({ + distinctId: user.id, + userId: user.id, + ownerType: 'personal', + authorizedRole: 'personal', + action: 'dismiss', + suggestionKind: 'coding_plan', + phase: 'accepted', + }); + }); + it('paginates filtered event history beyond the first 50 rows', async () => { - const user = await insertTestUser(); + const user = await insertTestUser({ is_admin: true }); await db.insert(cost_insight_events).values( Array.from({ length: 62 }, (_, index) => ({ owned_by_user_id: user.id, diff --git a/apps/web/src/routers/cost-insights-router.ts b/apps/web/src/routers/cost-insights-router.ts index 5fe1fc1900..4429f2c607 100644 --- a/apps/web/src/routers/cost-insights-router.ts +++ b/apps/web/src/routers/cost-insights-router.ts @@ -2,7 +2,7 @@ import { TRPCError } from '@trpc/server'; import * as z from 'zod'; import { db } from '@/lib/drizzle'; -import { createTRPCRouter, baseProcedure } from '@/lib/trpc/init'; +import { adminProcedure, createTRPCRouter } from '@/lib/trpc/init'; import { buildCostInsightsDashboardData, buildCostInsightsEventHistoryData, @@ -11,6 +11,7 @@ import { import { acknowledgeCostInsightAlert, clearCostInsightAlertState, + clearCostInsightAnomalyEpisode, clearCostInsightThresholdEpisode, createCostInsightEvent, countOpenCostInsightReviewItems, @@ -19,15 +20,31 @@ import { } from '@/lib/cost-insights/repository'; import { evaluateCostInsightsForOwner } from '@/lib/cost-insights/evaluation'; import { parseSpendThresholdUsd } from '@/lib/cost-insights/policy'; +import { + trackCostInsightsAlertAction, + trackCostInsightsSettingsSaved, + trackCostInsightsSuggestionAction, + trackCostInsightsUiInteraction, + type CostInsightsTrackingContext, +} from '@/lib/cost-insights/posthog-tracking'; +import { + CostInsightsSuggestionCtaSchema, + CostInsightsUiInteractionSchema, + OrganizationCostInsightsSuggestionCtaSchema, + OrganizationCostInsightsUiInteractionSchema, +} from '@/lib/cost-insights/tracking'; const UpdateCostInsightsSettingsSchema = z.object({ spendAlertsEnabled: z.boolean(), + anomalyAlertsEnabled: z.boolean(), costSuggestionsEnabled: z.boolean(), spendThresholdUsd: z.string().nullable(), + spend7DayThresholdUsd: z.string().nullable(), + spend30DayThresholdUsd: z.string().nullable(), }); const AcknowledgeCostInsightAlertSchema = z.object({ - alertKind: z.enum(['anomaly', 'threshold']), + alertKind: z.enum(['anomaly', 'threshold', 'threshold_7d', 'threshold_30d']), }); const DismissCostInsightSuggestionSchema = z.object({ @@ -40,16 +57,97 @@ const CostInsightEventHistorySchema = z.object({ pageSize: z.number().int().min(1).max(50), }); +function personalTrackingContext(userId: string): CostInsightsTrackingContext { + return { + distinctId: userId, + userId, + ownerType: 'personal', + authorizedRole: 'personal', + }; +} + +function booleanTransition(previous: boolean, current: boolean) { + if (previous === current) return 'unchanged' as const; + return current ? ('enabled' as const) : ('disabled' as const); +} + +function thresholdTransition(previous: number | null, current: number | null) { + if (previous === current) return 'unchanged' as const; + if (previous === null) return 'added' as const; + if (current === null) return 'removed' as const; + return 'changed' as const; +} + +function trackSettingsSaved( + trackingContext: CostInsightsTrackingContext, + previous: { + spend_alerts_enabled: boolean; + anomaly_alerts_enabled: boolean; + cost_suggestions_enabled: boolean; + spend_threshold_microdollars: number | null; + spend_7_day_threshold_microdollars: number | null; + spend_30_day_threshold_microdollars: number | null; + }, + current: { + spend_alerts_enabled: boolean; + anomaly_alerts_enabled: boolean; + cost_suggestions_enabled: boolean; + spend_threshold_microdollars: number | null; + spend_7_day_threshold_microdollars: number | null; + spend_30_day_threshold_microdollars: number | null; + } +) { + trackCostInsightsSettingsSaved({ + ...trackingContext, + spendAlertsTransition: booleanTransition( + previous.spend_alerts_enabled, + current.spend_alerts_enabled + ), + anomalyAlertsTransition: booleanTransition( + previous.anomaly_alerts_enabled, + current.anomaly_alerts_enabled + ), + costSuggestionsTransition: booleanTransition( + previous.cost_suggestions_enabled, + current.cost_suggestions_enabled + ), + threshold24hTransition: thresholdTransition( + previous.spend_threshold_microdollars, + current.spend_threshold_microdollars + ), + threshold7dTransition: thresholdTransition( + previous.spend_7_day_threshold_microdollars, + current.spend_7_day_threshold_microdollars + ), + threshold30dTransition: thresholdTransition( + previous.spend_30_day_threshold_microdollars, + current.spend_30_day_threshold_microdollars + ), + spendAlertsEnabled: current.spend_alerts_enabled, + anomalyAlertsEnabled: current.anomaly_alerts_enabled, + costSuggestionsEnabled: current.cost_suggestions_enabled, + threshold24hConfigured: current.spend_threshold_microdollars !== null, + threshold7dConfigured: current.spend_7_day_threshold_microdollars !== null, + threshold30dConfigured: current.spend_30_day_threshold_microdollars !== null, + }); +} + function changedFields( previous: { spend_alerts_enabled: boolean; + anomaly_alerts_enabled: boolean; cost_suggestions_enabled: boolean; spend_threshold_microdollars: number | null; + spend_7_day_threshold_microdollars: number | null; + spend_30_day_threshold_microdollars: number | null; }, current: { spend_alerts_enabled: boolean; + anomaly_alerts_enabled: boolean; cost_suggestions_enabled: boolean; spend_threshold_microdollars: number | null; + spend_7_day_threshold_microdollars: number | null; + spend_30_day_threshold_microdollars: number | null; } ) { const fields: Record = {}; @@ -59,6 +157,12 @@ function changedFields( new: current.spend_alerts_enabled, }; } + if (previous.anomaly_alerts_enabled !== current.anomaly_alerts_enabled) { + fields.anomalyAlertsEnabled = { + old: previous.anomaly_alerts_enabled, + new: current.anomaly_alerts_enabled, + }; + } if (previous.cost_suggestions_enabled !== current.cost_suggestions_enabled) { fields.costSuggestionsEnabled = { old: previous.cost_suggestions_enabled, @@ -71,17 +175,54 @@ function changedFields( new: current.spend_threshold_microdollars, }; } + if (previous.spend_7_day_threshold_microdollars !== current.spend_7_day_threshold_microdollars) { + fields.spend7DayThresholdMicrodollars = { + old: previous.spend_7_day_threshold_microdollars, + new: current.spend_7_day_threshold_microdollars, + }; + } + if ( + previous.spend_30_day_threshold_microdollars !== current.spend_30_day_threshold_microdollars + ) { + fields.spend30DayThresholdMicrodollars = { + old: previous.spend_30_day_threshold_microdollars, + new: current.spend_30_day_threshold_microdollars, + }; + } return fields; } +function settingsSnapshot(config: { + spend_alerts_enabled: boolean; + anomaly_alerts_enabled: boolean; + cost_suggestions_enabled: boolean; + spend_threshold_microdollars: number | null; + spend_7_day_threshold_microdollars: number | null; + spend_30_day_threshold_microdollars: number | null; +}) { + return { + spendAlertsEnabled: config.spend_alerts_enabled, + anomalyAlertsEnabled: config.anomaly_alerts_enabled, + costSuggestionsEnabled: config.cost_suggestions_enabled, + spendThresholdMicrodollars: config.spend_threshold_microdollars, + spend7DayThresholdMicrodollars: config.spend_7_day_threshold_microdollars, + spend30DayThresholdMicrodollars: config.spend_30_day_threshold_microdollars, + }; +} + async function updateOwnerSettings(params: { owner: { type: 'user'; id: string } | { type: 'organization'; id: string }; actorUserId: string; + trackingContext: CostInsightsTrackingContext; input: z.infer; }) { let spendThresholdMicrodollars: number | null; + let spend7DayThresholdMicrodollars: number | null; + let spend30DayThresholdMicrodollars: number | null; try { spendThresholdMicrodollars = parseSpendThresholdUsd(params.input.spendThresholdUsd); + spend7DayThresholdMicrodollars = parseSpendThresholdUsd(params.input.spend7DayThresholdUsd); + spend30DayThresholdMicrodollars = parseSpendThresholdUsd(params.input.spend30DayThresholdUsd); } catch (error) { throw new TRPCError({ code: 'BAD_REQUEST', @@ -91,8 +232,11 @@ async function updateOwnerSettings(params: { const { previous, current } = await updateCostInsightOwnerConfig(db, params.owner, { spendAlertsEnabled: params.input.spendAlertsEnabled, + anomalyAlertsEnabled: params.input.anomalyAlertsEnabled, costSuggestionsEnabled: params.input.costSuggestionsEnabled, spendThresholdMicrodollars, + spend7DayThresholdMicrodollars, + spend30DayThresholdMicrodollars, }); const changes = changedFields(previous, current); @@ -107,11 +251,7 @@ async function updateOwnerSettings(params: { description: 'Spend Alerts were disabled. Cost evidence remains visible.', snapshot: { changedFields: changes, - settings: { - spendAlertsEnabled: current.spend_alerts_enabled, - costSuggestionsEnabled: current.cost_suggestions_enabled, - spendThresholdMicrodollars: current.spend_threshold_microdollars, - }, + settings: settingsSnapshot(current), }, }); } else if (hasChanges && (previous.spend_alerts_enabled || current.spend_alerts_enabled)) { @@ -123,38 +263,55 @@ async function updateOwnerSettings(params: { description: 'Spend Alert settings were updated.', snapshot: { changedFields: changes, - settings: { - spendAlertsEnabled: current.spend_alerts_enabled, - costSuggestionsEnabled: current.cost_suggestions_enabled, - spendThresholdMicrodollars: current.spend_threshold_microdollars, - }, + settings: settingsSnapshot(current), }, }); } + if (previous.anomaly_alerts_enabled && !current.anomaly_alerts_enabled) { + await clearCostInsightAnomalyEpisode(db, params.owner); + } if ( previous.spend_threshold_microdollars !== null && current.spend_threshold_microdollars === null ) { - await clearCostInsightThresholdEpisode(db, params.owner, null); + await clearCostInsightThresholdEpisode(db, params.owner, null, 'threshold'); + } + if ( + previous.spend_7_day_threshold_microdollars !== null && + current.spend_7_day_threshold_microdollars === null + ) { + await clearCostInsightThresholdEpisode(db, params.owner, null, 'threshold_7d'); + } + if ( + previous.spend_30_day_threshold_microdollars !== null && + current.spend_30_day_threshold_microdollars === null + ) { + await clearCostInsightThresholdEpisode(db, params.owner, null, 'threshold_30d'); } if (current.spend_alerts_enabled) { await evaluateCostInsightsForOwner(db, params.owner); } + if (hasChanges) { + trackSettingsSaved(params.trackingContext, previous, current); + } return { success: true }; } async function disableOwnerThreshold(params: { owner: { type: 'user'; id: string } | { type: 'organization'; id: string }; actorUserId: string; + trackingContext: CostInsightsTrackingContext; }) { - return await db.transaction(async database => { + const result = await db.transaction(async database => { const { previous, current } = await updateCostInsightOwnerConfig(database, params.owner, { spendThresholdMicrodollars: null, }); await clearCostInsightThresholdEpisode(database, params.owner, null); - if (previous.spend_threshold_microdollars === null) return { success: true }; + if (previous.spend_threshold_microdollars === null) { + return { success: true, previous, current, changed: false }; + } if (current.spend_alerts_enabled) { await createCostInsightEvent(database, { @@ -165,35 +322,52 @@ async function disableOwnerThreshold(params: { description: 'Spend threshold was turned off.', snapshot: { changedFields: changedFields(previous, current), - settings: { - spendAlertsEnabled: current.spend_alerts_enabled, - costSuggestionsEnabled: current.cost_suggestions_enabled, - spendThresholdMicrodollars: current.spend_threshold_microdollars, - }, + settings: settingsSnapshot(current), }, }); } - return { success: true }; + return { success: true, previous, current, changed: true }; }); + if (result.changed) { + trackSettingsSaved(params.trackingContext, result.previous, result.current); + } + return { success: result.success }; } export const costInsightsRouter = createTRPCRouter({ - getDashboard: baseProcedure.query(async ({ ctx }) => { + trackUiInteraction: adminProcedure + .input(CostInsightsUiInteractionSchema) + .mutation(async ({ ctx, input }) => { + trackCostInsightsUiInteraction(personalTrackingContext(ctx.user.id), input); + return { success: true }; + }), + trackSuggestionCta: adminProcedure + .input(CostInsightsSuggestionCtaSchema) + .mutation(async ({ ctx, input }) => { + trackCostInsightsSuggestionAction({ + ...personalTrackingContext(ctx.user.id), + action: 'open_cta', + suggestionKind: input.suggestionKind, + phase: 'clicked', + }); + return { success: true }; + }), + getDashboard: adminProcedure.query(async ({ ctx }) => { return await buildCostInsightsDashboardData({ database: db, owner: { type: 'user', id: ctx.user.id }, uiOwner: { type: 'personal', name: ctx.user.google_user_name, authorizedRole: 'personal' }, }); }), - getSettings: baseProcedure.query(async ({ ctx }) => { + getSettings: adminProcedure.query(async ({ ctx }) => { return await buildCostInsightsSettingsData({ database: db, owner: { type: 'user', id: ctx.user.id }, uiOwner: { type: 'personal', name: ctx.user.google_user_name, authorizedRole: 'personal' }, }); }), - listEvents: baseProcedure.input(CostInsightEventHistorySchema).query(async ({ ctx, input }) => { + listEvents: adminProcedure.input(CostInsightEventHistorySchema).query(async ({ ctx, input }) => { return await buildCostInsightsEventHistoryData({ database: db, owner: { type: 'user', id: ctx.user.id }, @@ -202,7 +376,7 @@ export const costInsightsRouter = createTRPCRouter({ pageSize: input.pageSize, }); }), - getAttentionState: baseProcedure.query(async ({ ctx }) => { + getAttentionState: adminProcedure.query(async ({ ctx }) => { const reviewItemCount = await countOpenCostInsightReviewItems(db, { type: 'user', id: ctx.user.id, @@ -212,39 +386,56 @@ export const costInsightsRouter = createTRPCRouter({ reviewItemCount, }; }), - updateSettings: baseProcedure + updateSettings: adminProcedure .input(UpdateCostInsightsSettingsSchema) .mutation(async ({ ctx, input }) => { return await updateOwnerSettings({ owner: { type: 'user', id: ctx.user.id }, actorUserId: ctx.user.id, + trackingContext: personalTrackingContext(ctx.user.id), input, }); }), - acknowledgeAlert: baseProcedure + acknowledgeAlert: adminProcedure .input(AcknowledgeCostInsightAlertSchema) .mutation(async ({ ctx, input }) => { - await acknowledgeCostInsightAlert(db, { + const acknowledged = await acknowledgeCostInsightAlert(db, { owner: { type: 'user', id: ctx.user.id }, alertKind: input.alertKind, actorUserId: ctx.user.id, }); + if (acknowledged) { + trackCostInsightsAlertAction({ + ...personalTrackingContext(ctx.user.id), + action: 'acknowledge', + alertKind: input.alertKind, + }); + } return { success: true }; }), - disableThreshold: baseProcedure.mutation(async ({ ctx }) => { + disableThreshold: adminProcedure.mutation(async ({ ctx }) => { return await disableOwnerThreshold({ owner: { type: 'user', id: ctx.user.id }, actorUserId: ctx.user.id, + trackingContext: personalTrackingContext(ctx.user.id), }); }), - dismissSuggestion: baseProcedure + dismissSuggestion: adminProcedure .input(DismissCostInsightSuggestionSchema) .mutation(async ({ ctx, input }) => { - await dismissCostInsightSuggestion(db, { + const suggestionKind = await dismissCostInsightSuggestion(db, { owner: { type: 'user', id: ctx.user.id }, suggestionId: input.suggestionId, actorUserId: ctx.user.id, }); + if (suggestionKind) { + trackCostInsightsSuggestionAction({ + ...personalTrackingContext(ctx.user.id), + action: 'dismiss', + suggestionKind, + phase: 'accepted', + }); + } return { success: true }; }), }); @@ -256,4 +447,8 @@ export const costInsightsRouterInternals = { AcknowledgeCostInsightAlertSchema, DismissCostInsightSuggestionSchema, CostInsightEventHistorySchema, + CostInsightsUiInteractionSchema, + CostInsightsSuggestionCtaSchema, + OrganizationCostInsightsUiInteractionSchema, + OrganizationCostInsightsSuggestionCtaSchema, }; diff --git a/apps/web/src/routers/organizations/organization-cost-insights-router.test.ts b/apps/web/src/routers/organizations/organization-cost-insights-router.test.ts new file mode 100644 index 0000000000..534767d7f1 --- /dev/null +++ b/apps/web/src/routers/organizations/organization-cost-insights-router.test.ts @@ -0,0 +1,188 @@ +import { jest } from '@jest/globals'; + +import { db } from '@/lib/drizzle'; +import { + hasCurrentCostInsightAccess, + listCostInsightNotificationRecipientUserIds, +} from '@/lib/cost-insights/repository'; +import { addUserToOrganization, createOrganization } from '@/lib/organizations/organizations'; +import type { createCallerForUser as CreateCallerForUser } from '@/routers/test-utils'; +import { insertTestUser } from '@/tests/helpers/user.helper'; + +jest.mock('@/lib/cost-insights/posthog-tracking', () => ({ + trackCostInsightsAlertAction: jest.fn(), + trackCostInsightsSettingsSaved: jest.fn(), + trackCostInsightsSuggestionAction: jest.fn(), + trackCostInsightsUiInteraction: jest.fn(), +})); + +const trackingMock: { + trackCostInsightsSuggestionAction: jest.Mock; + trackCostInsightsUiInteraction: jest.Mock; +} = jest.requireMock('@/lib/cost-insights/posthog-tracking'); + +let createCallerForUser: typeof CreateCallerForUser; + +beforeAll(async () => { + ({ createCallerForUser } = await import('@/routers/test-utils')); +}); + +describe('Organization Cost Insights tracking', () => { + beforeEach(() => { + trackingMock.trackCostInsightsSuggestionAction.mockClear(); + trackingMock.trackCostInsightsUiInteraction.mockClear(); + }); + + it('attributes UI interactions to the acting admin organization owner', async () => { + const owner = await insertTestUser({ is_admin: true }); + const organization = await createOrganization('Cost Insights Tracking Org', owner.id); + const caller = await createCallerForUser(owner.id); + + await expect( + caller.organizations.costInsights.trackUiInteraction({ + organizationId: organization.id, + interaction: 'activity_filter_selected', + filter: 'alerts', + }) + ).resolves.toEqual({ success: true }); + expect(trackingMock.trackCostInsightsUiInteraction).toHaveBeenCalledWith( + { + distinctId: owner.id, + userId: owner.id, + ownerType: 'organization', + organizationId: organization.id, + authorizedRole: 'owner', + }, + { + organizationId: organization.id, + interaction: 'activity_filter_selected', + filter: 'alerts', + } + ); + }); + + it('allows admin billing managers to track suggestion CTA engagement', async () => { + const owner = await insertTestUser(); + const billingManager = await insertTestUser({ is_admin: true }); + const organization = await createOrganization('Cost Insights Billing Org', owner.id); + await addUserToOrganization(organization.id, billingManager.id, 'billing_manager'); + const caller = await createCallerForUser(billingManager.id); + + await expect( + caller.organizations.costInsights.trackSuggestionCta({ + organizationId: organization.id, + suggestionKind: 'kilo_pass', + }) + ).resolves.toEqual({ success: true }); + expect(trackingMock.trackCostInsightsSuggestionAction).toHaveBeenCalledWith({ + distinctId: billingManager.id, + userId: billingManager.id, + ownerType: 'organization', + organizationId: organization.id, + authorizedRole: 'billing_manager', + action: 'open_cta', + suggestionKind: 'kilo_pass', + phase: 'clicked', + }); + }); + + it('limits notification access to admin owners and billing managers', async () => { + const owner = await insertTestUser(); + const adminBillingManager = await insertTestUser({ is_admin: true }); + const nonAdminBillingManager = await insertTestUser(); + const adminMember = await insertTestUser({ is_admin: true }); + const adminPersonalOwner = await insertTestUser({ is_admin: true }); + const organization = await createOrganization('Cost Insights Notification Org', owner.id); + await addUserToOrganization(organization.id, adminBillingManager.id, 'billing_manager'); + await addUserToOrganization(organization.id, nonAdminBillingManager.id, 'billing_manager'); + await addUserToOrganization(organization.id, adminMember.id, 'member'); + + await expect( + listCostInsightNotificationRecipientUserIds(db, { + type: 'organization', + id: organization.id, + }) + ).resolves.toEqual([adminBillingManager.id]); + await expect( + hasCurrentCostInsightAccess( + db, + { type: 'organization', id: organization.id }, + adminBillingManager.id + ) + ).resolves.toBe(true); + await expect( + hasCurrentCostInsightAccess(db, { type: 'organization', id: organization.id }, owner.id) + ).resolves.toBe(false); + await expect( + hasCurrentCostInsightAccess(db, { type: 'organization', id: organization.id }, adminMember.id) + ).resolves.toBe(false); + await expect( + listCostInsightNotificationRecipientUserIds(db, { type: 'user', id: owner.id }) + ).resolves.toEqual([]); + await expect( + listCostInsightNotificationRecipientUserIds(db, { + type: 'user', + id: adminPersonalOwner.id, + }) + ).resolves.toEqual([adminPersonalOwner.id]); + }); + + it('rejects every Cost Insights procedure for non-admin organization owners', async () => { + const owner = await insertTestUser(); + const organization = await createOrganization('Cost Insights Non-Admin Org', owner.id); + const caller = await createCallerForUser(owner.id); + const organizationId = organization.id; + const calls = [ + () => + caller.organizations.costInsights.trackUiInteraction({ + organizationId, + interaction: 'activity_viewed' as const, + }), + () => + caller.organizations.costInsights.trackSuggestionCta({ + organizationId, + suggestionKind: 'kilo_pass' as const, + }), + () => caller.organizations.costInsights.getDashboard({ organizationId }), + () => caller.organizations.costInsights.getSettings({ organizationId }), + () => + caller.organizations.costInsights.listEvents({ + organizationId, + filter: 'all', + page: 1, + pageSize: 10, + }), + () => caller.organizations.costInsights.getAttentionState({ organizationId }), + () => + caller.organizations.costInsights.updateSettings({ + organizationId, + spendAlertsEnabled: false, + anomalyAlertsEnabled: true, + costSuggestionsEnabled: true, + spendThresholdUsd: null, + spend7DayThresholdUsd: null, + spend30DayThresholdUsd: null, + }), + () => + caller.organizations.costInsights.acknowledgeAlert({ + organizationId, + alertKind: 'anomaly', + }), + () => caller.organizations.costInsights.disableThreshold({ organizationId }), + () => + caller.organizations.costInsights.dismissSuggestion({ + organizationId, + suggestionId: crypto.randomUUID(), + }), + ]; + + for (const call of calls) { + await expect(call()).rejects.toMatchObject({ + code: 'FORBIDDEN', + message: 'Admin access required', + }); + } + expect(trackingMock.trackCostInsightsUiInteraction).not.toHaveBeenCalled(); + expect(trackingMock.trackCostInsightsSuggestionAction).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/routers/organizations/organization-cost-insights-router.ts b/apps/web/src/routers/organizations/organization-cost-insights-router.ts index cb598ce1f1..d54b4de245 100644 --- a/apps/web/src/routers/organizations/organization-cost-insights-router.ts +++ b/apps/web/src/routers/organizations/organization-cost-insights-router.ts @@ -3,7 +3,7 @@ import { and, eq } from 'drizzle-orm'; import { organization_memberships, organizations } from '@kilocode/db/schema'; import { db } from '@/lib/drizzle'; -import { createTRPCRouter, baseProcedure, type TRPCContext } from '@/lib/trpc/init'; +import { adminProcedure, createTRPCRouter, type TRPCContext } from '@/lib/trpc/init'; import { buildCostInsightsDashboardData, buildCostInsightsEventHistoryData, @@ -14,6 +14,13 @@ import { countOpenCostInsightReviewItems, dismissCostInsightSuggestion, } from '@/lib/cost-insights/repository'; +import { + trackCostInsightsAlertAction, + trackCostInsightsSuggestionAction, + trackCostInsightsUiInteraction, + type CostInsightsAuthorizedRole, + type CostInsightsTrackingContext, +} from '@/lib/cost-insights/posthog-tracking'; import { ensureOrganizationAccess, OrganizationIdInputSchema } from './utils'; import { costInsightsRouterInternals } from '../cost-insights-router'; @@ -55,13 +62,37 @@ async function resolveOrgReadContext(ctx: TRPCContext, organizationId: string) { } const role = await ensureOrganizationAccess(ctx, organizationId, ['owner', 'billing_manager']); + if (role !== 'owner' && role !== 'billing_manager') { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Only an organization owner or billing manager can view Cost Insights.', + }); + } return { name, authorizedRole: role, readOnly: false } as const; } +function organizationTrackingContext( + userId: string, + organizationId: string, + authorizedRole: CostInsightsAuthorizedRole +): CostInsightsTrackingContext { + return { + distinctId: userId, + userId, + ownerType: 'organization', + organizationId, + authorizedRole, + }; +} + async function ensureOrgManageAccess(ctx: TRPCContext, organizationId: string) { if (!ctx.user.is_admin) { - await ensureOrganizationAccess(ctx, organizationId, ['owner', 'billing_manager']); - return; + const role = await ensureOrganizationAccess(ctx, organizationId, ['owner', 'billing_manager']); + if (role === 'owner' || role === 'billing_manager') return role; + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Only an organization owner or billing manager can change Cost Insights.', + }); } const directRole = await getDirectCostInsightsRole(organizationId, ctx.user.id); if (directRole !== 'owner' && directRole !== 'billing_manager') { @@ -70,10 +101,33 @@ async function ensureOrgManageAccess(ctx: TRPCContext, organizationId: string) { message: 'Only an organization owner or billing manager can change Cost Insights.', }); } + return directRole; } export const organizationCostInsightsRouter = createTRPCRouter({ - getDashboard: baseProcedure.input(OrganizationIdInputSchema).query(async ({ ctx, input }) => { + trackUiInteraction: adminProcedure + .input(costInsightsRouterInternals.OrganizationCostInsightsUiInteractionSchema) + .mutation(async ({ ctx, input }) => { + const access = await resolveOrgReadContext(ctx, input.organizationId); + trackCostInsightsUiInteraction( + organizationTrackingContext(ctx.user.id, input.organizationId, access.authorizedRole), + input + ); + return { success: true }; + }), + trackSuggestionCta: adminProcedure + .input(costInsightsRouterInternals.OrganizationCostInsightsSuggestionCtaSchema) + .mutation(async ({ ctx, input }) => { + const role = await ensureOrgManageAccess(ctx, input.organizationId); + trackCostInsightsSuggestionAction({ + ...organizationTrackingContext(ctx.user.id, input.organizationId, role), + action: 'open_cta', + suggestionKind: input.suggestionKind, + phase: 'clicked', + }); + return { success: true }; + }), + getDashboard: adminProcedure.input(OrganizationIdInputSchema).query(async ({ ctx, input }) => { const access = await resolveOrgReadContext(ctx, input.organizationId); return await buildCostInsightsDashboardData({ database: db, @@ -85,7 +139,7 @@ export const organizationCostInsightsRouter = createTRPCRouter({ }, }); }), - getSettings: baseProcedure.input(OrganizationIdInputSchema).query(async ({ ctx, input }) => { + getSettings: adminProcedure.input(OrganizationIdInputSchema).query(async ({ ctx, input }) => { const access = await resolveOrgReadContext(ctx, input.organizationId); return await buildCostInsightsSettingsData({ database: db, @@ -98,7 +152,7 @@ export const organizationCostInsightsRouter = createTRPCRouter({ readOnly: access.readOnly, }); }), - listEvents: baseProcedure + listEvents: adminProcedure .input( OrganizationIdInputSchema.merge(costInsightsRouterInternals.CostInsightEventHistorySchema) ) @@ -112,7 +166,7 @@ export const organizationCostInsightsRouter = createTRPCRouter({ pageSize: input.pageSize, }); }), - getAttentionState: baseProcedure + getAttentionState: adminProcedure .input(OrganizationIdInputSchema) .query(async ({ ctx, input }) => { await resolveOrgReadContext(ctx, input.organizationId); @@ -125,53 +179,70 @@ export const organizationCostInsightsRouter = createTRPCRouter({ reviewItemCount, }; }), - updateSettings: baseProcedure + updateSettings: adminProcedure .input( OrganizationIdInputSchema.merge(costInsightsRouterInternals.UpdateCostInsightsSettingsSchema) ) .mutation(async ({ ctx, input }) => { - await ensureOrgManageAccess(ctx, input.organizationId); + const role = await ensureOrgManageAccess(ctx, input.organizationId); return await costInsightsRouterInternals.updateOwnerSettings({ owner: { type: 'organization', id: input.organizationId }, actorUserId: ctx.user.id, + trackingContext: organizationTrackingContext(ctx.user.id, input.organizationId, role), input, }); }), - acknowledgeAlert: baseProcedure + acknowledgeAlert: adminProcedure .input( OrganizationIdInputSchema.merge(costInsightsRouterInternals.AcknowledgeCostInsightAlertSchema) ) .mutation(async ({ ctx, input }) => { - await ensureOrgManageAccess(ctx, input.organizationId); - await acknowledgeCostInsightAlert(db, { + const role = await ensureOrgManageAccess(ctx, input.organizationId); + const acknowledged = await acknowledgeCostInsightAlert(db, { owner: { type: 'organization', id: input.organizationId }, alertKind: input.alertKind, actorUserId: ctx.user.id, }); + if (acknowledged) { + trackCostInsightsAlertAction({ + ...organizationTrackingContext(ctx.user.id, input.organizationId, role), + action: 'acknowledge', + alertKind: input.alertKind, + }); + } return { success: true }; }), - disableThreshold: baseProcedure + disableThreshold: adminProcedure .input(OrganizationIdInputSchema) .mutation(async ({ ctx, input }) => { - await ensureOrgManageAccess(ctx, input.organizationId); + const role = await ensureOrgManageAccess(ctx, input.organizationId); return await costInsightsRouterInternals.disableOwnerThreshold({ owner: { type: 'organization', id: input.organizationId }, actorUserId: ctx.user.id, + trackingContext: organizationTrackingContext(ctx.user.id, input.organizationId, role), }); }), - dismissSuggestion: baseProcedure + dismissSuggestion: adminProcedure .input( OrganizationIdInputSchema.merge( costInsightsRouterInternals.DismissCostInsightSuggestionSchema ) ) .mutation(async ({ ctx, input }) => { - await ensureOrgManageAccess(ctx, input.organizationId); - await dismissCostInsightSuggestion(db, { + const role = await ensureOrgManageAccess(ctx, input.organizationId); + const suggestionKind = await dismissCostInsightSuggestion(db, { owner: { type: 'organization', id: input.organizationId }, suggestionId: input.suggestionId, actorUserId: ctx.user.id, }); + if (suggestionKind) { + trackCostInsightsSuggestionAction({ + ...organizationTrackingContext(ctx.user.id, input.organizationId, role), + action: 'dismiss', + suggestionKind, + phase: 'accepted', + }); + } return { success: true }; }), }); diff --git a/dev/seed/cost-insights/spend-evidence.ts b/dev/seed/cost-insights/spend-evidence.ts index e596b97b3d..74aaa3dad8 100644 --- a/dev/seed/cost-insights/spend-evidence.ts +++ b/dev/seed/cost-insights/spend-evidence.ts @@ -3,12 +3,16 @@ import { createHash, randomUUID } from 'node:crypto'; import { computeDatabaseUrl } from '@kilocode/db'; import { captureCostInsightSpend, + COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, COST_INSIGHT_DRIVER_FALLBACK, + COST_INSIGHT_EXA_PRODUCT_KEY, COST_INSIGHT_KILOCLAW_PRODUCT_KEY, type CostInsightSpendOwner, } from '@kilocode/db/cost-insights-rollups'; import { api_kind, + coding_plan_subscriptions, + coding_plan_terms, cost_insight_active_suggestions, cost_insight_events, cost_insight_notification_deliveries, @@ -17,7 +21,9 @@ import { cost_insight_owner_hour_totals, cost_insight_owner_states, cost_insight_rollup_coverage, + cost_insight_rollup_degraded_intervals, credit_transactions, + exa_usage_log, feature, kilocode_users, microdollar_usage, @@ -27,10 +33,11 @@ import { organizations, type CostInsightEventSnapshot, } from '@kilocode/db/schema'; -import type { GatewayApiKind } from '@kilocode/db/schema-types'; -import { eq, inArray, like, or, sql } from 'drizzle-orm'; +import type { CodingPlanTermKind, GatewayApiKind } from '@kilocode/db/schema-types'; +import { and, eq, inArray, like, lt, or, sql } from 'drizzle-orm'; import { getSeedDb } from '../lib/db'; +import { createSeedStripeCustomer, deleteSeedStripeCustomer } from '../lib/stripe'; import type { SeedResult } from '../index'; const HOUR_MS = 60 * 60 * 1_000; @@ -38,6 +45,24 @@ const DAY_MS = 24 * HOUR_MS; const COVERAGE_DAYS = 90; const BALANCE_BUFFER_MICRODOLLARS = 100_000_000; const CREDIT_CATEGORY_PREFIX = 'dev-seed:cost-insights'; +const CODING_PLAN_ID = 'minimax-token-plan-plus'; +const CODING_PLAN_PROVIDER_ID = 'minimax'; +const CODING_PLAN_COST_MICRODOLLARS = 20_000_000; +const UNKNOWN_FEATURE_KEY = 'dev-seed-cost-insights-unknown'; +const DEGRADED_INTERVAL_ID = '4f2fc143-4b30-4c8a-878b-df89c89c6780'; +type RollupMode = + | 'bootstrap' + | 'healthy' + | 'repairable-drift' + | 'unknown-taxonomy' + | 'degraded-late'; +const ROLLUP_MODES: RollupMode[] = [ + 'bootstrap', + 'healthy', + 'repairable-drift', + 'unknown-taxonomy', + 'degraded-late', +]; const PERSONAL_OWNER_ID = '4f2fc143-4b30-4c8a-878b-df89c89c6701'; const BILLING_MANAGER_ID = '4f2fc143-4b30-4c8a-878b-df89c89c6702'; @@ -56,7 +81,8 @@ const ORGANIZATION_OWNER: CostInsightSpendOwner = { }; const SEED_USER_IDS = [PERSONAL_OWNER_ID, BILLING_MANAGER_ID, ORGANIZATION_MEMBER_ID]; -export const usage = ''; +export const usage = + '[--rollup-mode ]'; type VariableDriver = { featureKey: string; @@ -81,6 +107,32 @@ type ScheduledSpendEvent = { planKey: 'standard' | 'commit'; }; +type ExaSpendEvent = { + owner: CostInsightSpendOwner; + actorUserId: string; + occurredAt: string; + amountMicrodollars: number; + path: '/search' | '/contents'; + featureKey: 'search' | 'contents'; +}; + +type CodingPlanSpendEvent = { + owner: CostInsightSpendOwner; + actorUserId: string; + occurredAt: string; + amountMicrodollars: number; + termKind: Extract; +}; + +type UnattributedVariableSpendEvent = { + owner: CostInsightSpendOwner; + actorUserId: string; + occurredAt: string; + amountMicrodollars: number; + modelKey: string; + providerKey: string; +}; + const PERSONAL_DRIVERS: VariableDriver[] = [ { featureKey: 'cli', @@ -124,21 +176,34 @@ const ORGANIZATION_DRIVERS: VariableDriver[] = [ ]; function printUsage(): void { - console.log('Usage: pnpm dev:seed cost-insights:spend-evidence'); + console.log(`Usage: pnpm dev:seed cost-insights:spend-evidence ${usage}`); console.log(''); console.log('Creates dedicated personal and organization Spend owners with 90 days of'); - console.log('canonical spend evidence and matching Cost Insights hourly rollups.'); + console.log('canonical spend evidence from AI Gateway, Exa, Coding Plan, and KiloClaw.'); console.log(''); - console.log('The fixture includes current-hour anomaly spikes, rolling 24-hour spend above'); - console.log('typical test thresholds, recurring Scheduled Credit spend, and organization'); - console.log("member driver attribution. Reruns replace only this fixture's data."); + console.log('Rollup modes:'); + console.log(' bootstrap Canonical history plus repairable drift; run backfill next.'); + console.log(' healthy Matching rollups and complete 90-day coverage.'); + console.log(' repairable-drift Complete coverage with missing, late, and stale rollups.'); + console.log(' unknown-taxonomy Healthy data plus one dry-run-only taxonomy diagnostic.'); + console.log(' degraded-late Late data plus unresolved degraded interval for inspection.'); + console.log(''); + console.log('Default: bootstrap. Reruns replace only this fixture data and v1 coverage state.'); } -function requireNoArguments(args: string[]): void { - if (args.length > 0) { +function parseRollupMode(args: string[]): RollupMode { + if (args.length === 0) return 'bootstrap'; + if (args.length !== 2 || args[0] !== '--rollup-mode') { printUsage(); throw new Error(`Unexpected arguments: ${args.join(' ')}`); } + const requestedMode = args[1]; + const rollupMode = ROLLUP_MODES.find(mode => mode === requestedMode); + if (!rollupMode) { + printUsage(); + throw new Error(`Unknown rollup mode: ${requestedMode}`); + } + return rollupMode; } function assertLocalDatabaseTarget(): { hostname: string; database: string; port: string } { @@ -169,6 +234,35 @@ function timestampAtHourOffset(currentHour: number, hourOffset: number): string return new Date(currentHour - hourOffset * HOUR_MS).toISOString(); } +async function ensureExaUsageLogPartitions( + database: Pick, 'execute'>, + timestamps: string[] +): Promise { + const monthStarts = new Map(); + for (const timestamp of timestamps) { + const date = new Date(timestamp); + const monthStart = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)); + monthStarts.set(monthStart.getTime(), monthStart); + } + + for (const monthStart of monthStarts.values()) { + const nextMonth = new Date( + Date.UTC(monthStart.getUTCFullYear(), monthStart.getUTCMonth() + 1, 1) + ); + const year = String(monthStart.getUTCFullYear()); + const month = String(monthStart.getUTCMonth() + 1).padStart(2, '0'); + const partitionName = `exa_usage_log_${year}_${month}`; + if (!/^exa_usage_log_\d{4}_(?:0[1-9]|1[0-2])$/.test(partitionName)) { + throw new Error(`Unsafe Exa usage-log partition name: ${partitionName}`); + } + await database.execute( + sql.raw( + `CREATE TABLE IF NOT EXISTS "public"."${partitionName}" PARTITION OF "public"."exa_usage_log" FOR VALUES FROM ('${monthStart.toISOString().slice(0, 10)}') TO ('${nextMonth.toISOString().slice(0, 10)}')` + ) + ); + } +} + function chooseByIndex(values: T[], index: number, label: string): T { const value = values[index % values.length]; if (value === undefined) { @@ -316,6 +410,69 @@ function buildScheduledSpendEvents(currentHour: number): ScheduledSpendEvent[] { ]; } +function buildExaSpendEvents(currentHour: number): ExaSpendEvent[] { + return [ + { + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: timestampAtHourOffset(currentHour, 6), + amountMicrodollars: 1_700_000, + path: '/search', + featureKey: 'search', + }, + { + owner: ORGANIZATION_OWNER, + actorUserId: ORGANIZATION_MEMBER_ID, + occurredAt: timestampAtHourOffset(currentHour, 9), + amountMicrodollars: 2_400_000, + path: '/contents', + featureKey: 'contents', + }, + ]; +} + +function buildCodingPlanSpendEvents(currentHour: number): CodingPlanSpendEvent[] { + return [ + { + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: timestampAtHourOffset(currentHour, 12), + amountMicrodollars: CODING_PLAN_COST_MICRODOLLARS, + termKind: 'activation', + }, + { + owner: ORGANIZATION_OWNER, + actorUserId: BILLING_MANAGER_ID, + occurredAt: timestampAtHourOffset(currentHour, 14), + amountMicrodollars: CODING_PLAN_COST_MICRODOLLARS, + termKind: 'renewal', + }, + ]; +} + +function buildUnattributedVariableSpendEvents( + currentHour: number +): UnattributedVariableSpendEvent[] { + return [ + { + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: timestampAtHourOffset(currentHour, 16), + amountMicrodollars: 620_000, + modelKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }, + { + owner: ORGANIZATION_OWNER, + actorUserId: ORGANIZATION_MEMBER_ID, + occurredAt: timestampAtHourOffset(currentHour, 17), + amountMicrodollars: 940_000, + modelKey: 'openai/gpt-4.1-mini', + providerKey: 'openai', + }, + ]; +} + function ownerColumns(owner: CostInsightSpendOwner): { organizationId: string | null; userId: string | null; @@ -386,8 +543,8 @@ function seedTopDrivers( modelOrPlanKey: 'openai/gpt-4.1', providerKey: 'openai', actorUserId: BILLING_MANAGER_ID, - totalMicrodollars: 18_000_000, - spendRecordCount: 1, + totalMicrodollars: 28_000_000, + spendRecordCount: 58, }, { spendCategory: 'variable', @@ -397,8 +554,8 @@ function seedTopDrivers( modelOrPlanKey: 'google/gemini-2.5-pro', providerKey: 'google', actorUserId: ORGANIZATION_MEMBER_ID, - totalMicrodollars: 15_000_000, - spendRecordCount: 1, + totalMicrodollars: 18_000_000, + spendRecordCount: 41, }, { spendCategory: 'scheduled', @@ -423,8 +580,8 @@ function seedTopDrivers( modelOrPlanKey: 'anthropic/claude-sonnet-4', providerKey: 'anthropic', actorUserId: PERSONAL_OWNER_ID, - totalMicrodollars: 12_000_000, - spendRecordCount: 1, + totalMicrodollars: 74_200_000, + spendRecordCount: 184, }, { spendCategory: 'variable', @@ -434,8 +591,19 @@ function seedTopDrivers( modelOrPlanKey: 'openai/gpt-4.1-mini', providerKey: 'openai', actorUserId: PERSONAL_OWNER_ID, - totalMicrodollars: 11_000_000, - spendRecordCount: 1, + totalMicrodollars: 28_500_000, + spendRecordCount: 61, + }, + { + spendCategory: 'variable', + source: 'other', + productKey: COST_INSIGHT_EXA_PRODUCT_KEY, + featureKey: 'search', + modelOrPlanKey: COST_INSIGHT_DRIVER_FALLBACK, + providerKey: COST_INSIGHT_EXA_PRODUCT_KEY, + actorUserId: PERSONAL_OWNER_ID, + totalMicrodollars: 10_000_000, + spendRecordCount: 25, }, { spendCategory: 'scheduled', @@ -461,15 +629,44 @@ export async function run(...args: string[]): Promise { printUsage(); return; } - requireNoArguments(args); + const rollupMode = parseRollupMode(args); const databaseTarget = assertLocalDatabaseTarget(); const db = getSeedDb(); const currentHour = floorUtcHour(Date.now()); const currentHourIso = new Date(currentHour).toISOString(); const coverageStartIso = new Date(currentHour - COVERAGE_DAYS * DAY_MS).toISOString(); + const maintenanceStartIso = timestampAtHourOffset(currentHour, 25); + const lateArrivalHourIso = timestampAtHourOffset(currentHour, 4); + const staleRollupHourIso = timestampAtHourOffset(currentHour, 25); const variableEvents = buildVariableSpendEvents(currentHour); const scheduledEvents = buildScheduledSpendEvents(currentHour); + const exaEvents = buildExaSpendEvents(currentHour); + const codingPlanEvents = buildCodingPlanSpendEvents(currentHour); + const unattributedVariableEvents = buildUnattributedVariableSpendEvents(currentHour); + const includesLateArrival = + rollupMode === 'bootstrap' || + rollupMode === 'repairable-drift' || + rollupMode === 'degraded-late'; + const includesUnknownTaxonomy = rollupMode === 'unknown-taxonomy'; + const lateArrivalEvent: UnattributedVariableSpendEvent = { + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: lateArrivalHourIso, + amountMicrodollars: 780_000, + modelKey: 'google/gemini-2.5-pro', + providerKey: 'google', + }; + const unknownTaxonomyEvent: VariableSpendEvent = { + owner: PERSONAL_OWNER, + actorUserId: PERSONAL_OWNER_ID, + occurredAt: timestampAtHourOffset(currentHour, 20), + amountMicrodollars: 510_000, + featureKey: UNKNOWN_FEATURE_KEY, + apiKind: 'messages', + modelKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }; const suggestionWindowStart = new Date(currentHour - 7 * DAY_MS).toISOString(); const suggestionWindowEnd = new Date(currentHour).toISOString(); const personalAnomalyEventId = randomUUID(); @@ -489,7 +686,7 @@ export async function run(...args: string[]): Promise { suggestion_key: suggestionKey(`personal:coding-plan:${currentHourIso}`), title: 'Get more MiniMax usage with Token Plan Plus', description: - 'You spent $15.00 on MiniMax in the last 7 days, about $64 over 30 days at the same pace. Token Plan Plus costs $20 every 30 days and includes about 1.7B M3 tokens with access to the full MiniMax model family.', + 'The plan includes about 1.7B M3 tokens and access to the full MiniMax model family.', cta_label: 'View MiniMax plan', cta_href: '/subscriptions', evidence_window_start: suggestionWindowStart, @@ -505,16 +702,16 @@ export async function run(...args: string[]): Promise { ...costInsightOwnerColumns(PERSONAL_OWNER), suggestion_kind: 'kilo_pass', suggestion_key: suggestionKey(`personal:kilo-pass:${currentHourIso}`), - title: 'Get more credits from your monthly spend with Kilo Pass Expert', + title: 'Get more credits with Kilo Pass Expert', description: - 'You spent $106.90 on pay-as-you-go credits in the last 7 days, about $458 over 30 days at the same pace. Kilo Pass Expert costs $199 per month and includes $199 in paid credits, plus up to $79.60 in free bonus credits. Based on your recent spend, the plan could give you more credits for part of the spend you already make.', + 'The plan includes $199 in paid credits plus up to $79.60 in free bonus credits.', cta_label: 'View Kilo Pass Expert', cta_href: '/subscriptions/kilo-pass', evidence_window_start: suggestionWindowStart, evidence_window_end: suggestionWindowEnd, observed_microdollars: 106_900_000, benefit_label: 'Expert plan', - benefit_detail: '$199 + up to $79.60 bonus', + benefit_detail: '$199/mo + up to $79.60 bonus', created_at: timestampAtHourOffset(currentHour, 1), updated_at: timestampAtHourOffset(currentHour, 1), }, @@ -523,16 +720,15 @@ export async function run(...args: string[]): Promise { ...costInsightOwnerColumns(ORGANIZATION_OWNER), suggestion_kind: 'coding_plan', suggestion_key: suggestionKey(`organization:coding-plan:${currentHourIso}`), - title: 'Review Coding Plan coverage for team runs', - description: - 'Cloud Agent and Security Agent usage from team members is concentrated in a few recurring workflows.', + title: 'Consider a Coding Plan for team runs', + description: 'A Coding Plan may improve cost efficiency for recurring team workflows.', cta_label: 'View subscriptions', cta_href: `/organizations/${ORGANIZATION_ID}/subscriptions`, evidence_window_start: suggestionWindowStart, evidence_window_end: suggestionWindowEnd, observed_microdollars: 318_000_000, - benefit_label: 'Observed spend', - benefit_detail: '$318 in 7 days', + benefit_label: 'Plan option', + benefit_detail: 'Compare Coding Plans', created_at: timestampAtHourOffset(currentHour, 2), updated_at: timestampAtHourOffset(currentHour, 2), }, @@ -550,7 +746,14 @@ export async function run(...args: string[]): Promise { currentHourVariableMicrodollars: 112_700_000, anomalyBaselineMicrodollars: 6_000_000, anomalyThresholdMicrodollars: 18_000_000, - topDrivers: seedTopDrivers(PERSONAL_OWNER), + topDrivers: seedTopDrivers(PERSONAL_OWNER).filter( + driver => driver.spendCategory === 'variable' + ), + topDriversWindow: { + startInclusive: currentHourIso, + endExclusive: new Date(currentHour + 42 * 60 * 1_000).toISOString(), + spendCategory: 'variable', + }, }, dedupe_key: `dev-seed:personal:anomaly:${currentHourIso}`, occurred_at: currentHourIso, @@ -567,6 +770,10 @@ export async function run(...args: string[]): Promise { rolling24HourMicrodollars: 184_900_000, thresholdMicrodollars: personalThresholdMicrodollars, topDrivers: seedTopDrivers(PERSONAL_OWNER), + topDriversWindow: { + startInclusive: new Date(currentHour - 24 * HOUR_MS + 42 * 60 * 1_000).toISOString(), + endExclusive: new Date(currentHour + 42 * 60 * 1_000).toISOString(), + }, }, dedupe_key: `dev-seed:personal:threshold:${currentHourIso}`, occurred_at: timestampAtHourOffset(currentHour, 1), @@ -598,7 +805,7 @@ export async function run(...args: string[]): Promise { active_suggestion_id: personalKiloPassSuggestionId, actor_user_id: null, title: 'Cost Suggestion created', - description: 'Get more credits from your monthly spend with Kilo Pass Expert', + description: 'Get more credits with Kilo Pass Expert', snapshot: { suggestion: { suggestionKey: suggestionKey(`personal:kilo-pass:${currentHourIso}`), @@ -670,7 +877,14 @@ export async function run(...args: string[]): Promise { currentHourVariableMicrodollars: 46_000_000, anomalyBaselineMicrodollars: 8_600_000, anomalyThresholdMicrodollars: 25_800_000, - topDrivers: seedTopDrivers(ORGANIZATION_OWNER), + topDrivers: seedTopDrivers(ORGANIZATION_OWNER).filter( + driver => driver.spendCategory === 'variable' + ), + topDriversWindow: { + startInclusive: currentHourIso, + endExclusive: new Date(currentHour + 42 * 60 * 1_000).toISOString(), + spendCategory: 'variable', + }, }, dedupe_key: `dev-seed:organization:anomaly:${currentHourIso}`, occurred_at: currentHourIso, @@ -687,6 +901,10 @@ export async function run(...args: string[]): Promise { rolling24HourMicrodollars: 128_000_000, thresholdMicrodollars: organizationThresholdMicrodollars, topDrivers: seedTopDrivers(ORGANIZATION_OWNER), + topDriversWindow: { + startInclusive: new Date(currentHour - 24 * HOUR_MS + 42 * 60 * 1_000).toISOString(), + endExclusive: new Date(currentHour + 42 * 60 * 1_000).toISOString(), + }, }, dedupe_key: `dev-seed:organization:threshold:${currentHourIso}`, occurred_at: timestampAtHourOffset(currentHour, 1), @@ -698,7 +916,7 @@ export async function run(...args: string[]): Promise { active_suggestion_id: organizationCodingPlanSuggestionId, actor_user_id: null, title: 'Cost Suggestion created', - description: 'Review Coding Plan coverage for team runs', + description: 'Consider a Coding Plan for team runs', snapshot: { suggestion: { suggestionKey: suggestionKey(`organization:coding-plan:${currentHourIso}`), @@ -767,260 +985,397 @@ export async function run(...args: string[]): Promise { { event_id: organizationThresholdEventId, recipient_user_id: BILLING_MANAGER_ID }, ] satisfies (typeof cost_insight_notification_deliveries.$inferInsert)[]; - const personalVariableMicrodollars = sumOwnerAmounts(variableEvents, PERSONAL_OWNER); - const personalScheduledMicrodollars = sumOwnerAmounts(scheduledEvents, PERSONAL_OWNER); - const organizationVariableMicrodollars = sumOwnerAmounts(variableEvents, ORGANIZATION_OWNER); - const organizationScheduledMicrodollars = sumOwnerAmounts(scheduledEvents, ORGANIZATION_OWNER); + const canonicalVariableSpendEvents = [ + ...variableEvents, + ...exaEvents, + ...unattributedVariableEvents, + ...(includesLateArrival ? [lateArrivalEvent] : []), + ...(includesUnknownTaxonomy ? [unknownTaxonomyEvent] : []), + ]; + const canonicalScheduledSpendEvents = [...scheduledEvents, ...codingPlanEvents]; + const personalVariableMicrodollars = sumOwnerAmounts( + canonicalVariableSpendEvents, + PERSONAL_OWNER + ); + const personalScheduledMicrodollars = sumOwnerAmounts( + canonicalScheduledSpendEvents, + PERSONAL_OWNER + ); + const organizationVariableMicrodollars = sumOwnerAmounts( + canonicalVariableSpendEvents, + ORGANIZATION_OWNER + ); + const organizationScheduledMicrodollars = sumOwnerAmounts( + canonicalScheduledSpendEvents, + ORGANIZATION_OWNER + ); - const featureKeys = [...new Set(variableEvents.map(event => event.featureKey))]; + const featureKeys = [ + ...new Set([ + ...variableEvents.map(event => event.featureKey), + ...(includesUnknownTaxonomy ? [UNKNOWN_FEATURE_KEY] : []), + ]), + ]; const apiKinds = [...new Set(variableEvents.map(event => event.apiKind))]; - await db.transaction(async tx => { - const seedUsageIds = tx - .select({ id: microdollar_usage.id }) - .from(microdollar_usage) - .where( - or( - inArray(microdollar_usage.kilo_user_id, SEED_USER_IDS), - eq(microdollar_usage.organization_id, ORGANIZATION_ID) - ) - ); - - await tx - .delete(microdollar_usage_metadata) - .where(inArray(microdollar_usage_metadata.id, seedUsageIds)); - await tx - .delete(microdollar_usage_daily) - .where( - or( - inArray(microdollar_usage_daily.kilo_user_id, SEED_USER_IDS), - eq(microdollar_usage_daily.organization_id, ORGANIZATION_ID) - ) - ); - await tx - .delete(microdollar_usage) - .where( - or( - inArray(microdollar_usage.kilo_user_id, SEED_USER_IDS), - eq(microdollar_usage.organization_id, ORGANIZATION_ID) - ) - ); - await tx - .delete(credit_transactions) - .where( - or( - like( - credit_transactions.credit_category, - `kiloclaw-subscription:${CREDIT_CATEGORY_PREFIX}:%` - ), - like( - credit_transactions.credit_category, - `kiloclaw-subscription-commit:${CREDIT_CATEGORY_PREFIX}:%` - ) - ) - ); + await ensureExaUsageLogPartitions( + db, + exaEvents.map(event => event.occurredAt) + ); - const seedCostInsightEventIds = tx - .select({ id: cost_insight_events.id }) - .from(cost_insight_events) - .where( - or( - eq(cost_insight_events.owned_by_user_id, PERSONAL_OWNER_ID), - eq(cost_insight_events.owned_by_organization_id, ORGANIZATION_ID) - ) - ); - await tx - .delete(cost_insight_notification_deliveries) - .where(inArray(cost_insight_notification_deliveries.event_id, seedCostInsightEventIds)); - await tx - .delete(cost_insight_owner_states) - .where( - or( - eq(cost_insight_owner_states.owned_by_user_id, PERSONAL_OWNER_ID), - eq(cost_insight_owner_states.owned_by_organization_id, ORGANIZATION_ID) - ) - ); - await tx - .delete(cost_insight_events) - .where( - or( - eq(cost_insight_events.owned_by_user_id, PERSONAL_OWNER_ID), - eq(cost_insight_events.owned_by_organization_id, ORGANIZATION_ID) - ) - ); - await tx - .delete(cost_insight_active_suggestions) - .where( - or( - eq(cost_insight_active_suggestions.owned_by_user_id, PERSONAL_OWNER_ID), - eq(cost_insight_active_suggestions.owned_by_organization_id, ORGANIZATION_ID) - ) - ); - await tx - .delete(cost_insight_owner_configs) - .where( - or( - eq(cost_insight_owner_configs.owned_by_user_id, PERSONAL_OWNER_ID), - eq(cost_insight_owner_configs.owned_by_organization_id, ORGANIZATION_ID) - ) - ); - await tx - .delete(cost_insight_owner_hour_driver_buckets) - .where( - or( - eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, PERSONAL_OWNER_ID), - eq(cost_insight_owner_hour_driver_buckets.owned_by_organization_id, ORGANIZATION_ID) - ) - ); - await tx - .delete(cost_insight_owner_hour_totals) - .where( - or( - eq(cost_insight_owner_hour_totals.owned_by_user_id, PERSONAL_OWNER_ID), - eq(cost_insight_owner_hour_totals.owned_by_organization_id, ORGANIZATION_ID) - ) - ); + const seedUserProfiles = [ + { + id: PERSONAL_OWNER_ID, + email: PERSONAL_OWNER_EMAIL, + name: 'Morgan Lee', + isAdmin: true, + }, + { + id: BILLING_MANAGER_ID, + email: BILLING_MANAGER_EMAIL, + name: 'Priya Shah', + isAdmin: true, + }, + { + id: ORGANIZATION_MEMBER_ID, + email: ORGANIZATION_MEMBER_EMAIL, + name: 'Diego Santos', + isAdmin: false, + }, + ]; + const previousSeedUsers = await db + .select({ + id: kilocode_users.id, + stripeCustomerId: kilocode_users.stripe_customer_id, + }) + .from(kilocode_users) + .where(inArray(kilocode_users.id, SEED_USER_IDS)); + const previousStripeCustomerIds = new Map( + previousSeedUsers.map(user => [user.id, user.stripeCustomerId]) + ); + const seedUsers: Array<{ + id: string; + email: string; + name: string; + isAdmin: boolean; + stripeCustomerId: string; + }> = []; + + try { + for (const user of seedUserProfiles) { + const stripeCustomer = await createSeedStripeCustomer({ + email: user.email, + name: user.name, + kiloUserId: user.id, + }); + seedUsers.push({ ...user, stripeCustomerId: stripeCustomer.id }); + } - const seedUsers = [ - { - id: PERSONAL_OWNER_ID, - email: PERSONAL_OWNER_EMAIL, - name: 'Morgan Lee', - stripeCustomerId: 'cus_dev_seed_cost_insights_owner', - }, - { - id: BILLING_MANAGER_ID, - email: BILLING_MANAGER_EMAIL, - name: 'Priya Shah', - stripeCustomerId: 'cus_dev_seed_cost_insights_billing', - }, - { - id: ORGANIZATION_MEMBER_ID, - email: ORGANIZATION_MEMBER_EMAIL, - name: 'Diego Santos', - stripeCustomerId: 'cus_dev_seed_cost_insights_member', - }, - ]; + await db.transaction(async tx => { + const seedUsageIds = tx + .select({ id: microdollar_usage.id }) + .from(microdollar_usage) + .where( + or( + inArray(microdollar_usage.kilo_user_id, SEED_USER_IDS), + eq(microdollar_usage.organization_id, ORGANIZATION_ID) + ) + ); - for (const user of seedUsers) { await tx - .insert(kilocode_users) - .values({ - id: user.id, - google_user_email: user.email, - google_user_name: user.name, - google_user_image_url: `https://example.com/dev-seed/${user.id}.png`, - stripe_customer_id: user.stripeCustomerId, - normalized_email: user.email, - has_validation_stytch: true, - customer_source: 'dev-seed', - microdollars_used: 0, - total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, - }) - .onConflictDoUpdate({ - target: kilocode_users.id, - set: { + .delete(microdollar_usage_metadata) + .where(inArray(microdollar_usage_metadata.id, seedUsageIds)); + await tx + .delete(microdollar_usage_daily) + .where( + or( + inArray(microdollar_usage_daily.kilo_user_id, SEED_USER_IDS), + eq(microdollar_usage_daily.organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(microdollar_usage) + .where( + or( + inArray(microdollar_usage.kilo_user_id, SEED_USER_IDS), + eq(microdollar_usage.organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(exa_usage_log) + .where( + or( + inArray(exa_usage_log.kilo_user_id, SEED_USER_IDS), + eq(exa_usage_log.organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(coding_plan_subscriptions) + .where(inArray(coding_plan_subscriptions.user_id, SEED_USER_IDS)); + await tx + .delete(credit_transactions) + .where( + or( + like( + credit_transactions.credit_category, + `kiloclaw-subscription:${CREDIT_CATEGORY_PREFIX}:%` + ), + like( + credit_transactions.credit_category, + `kiloclaw-subscription-commit:${CREDIT_CATEGORY_PREFIX}:%` + ), + like(credit_transactions.credit_category, `coding-plan:${CREDIT_CATEGORY_PREFIX}:%`) + ) + ); + await tx + .delete(cost_insight_rollup_degraded_intervals) + .where(eq(cost_insight_rollup_degraded_intervals.id, DEGRADED_INTERVAL_ID)); + + const seedCostInsightEventIds = tx + .select({ id: cost_insight_events.id }) + .from(cost_insight_events) + .where( + or( + eq(cost_insight_events.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_events.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(cost_insight_notification_deliveries) + .where(inArray(cost_insight_notification_deliveries.event_id, seedCostInsightEventIds)); + await tx + .delete(cost_insight_owner_states) + .where( + or( + eq(cost_insight_owner_states.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_states.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(cost_insight_events) + .where( + or( + eq(cost_insight_events.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_events.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(cost_insight_active_suggestions) + .where( + or( + eq(cost_insight_active_suggestions.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_active_suggestions.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(cost_insight_owner_configs) + .where( + or( + eq(cost_insight_owner_configs.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_configs.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(cost_insight_owner_hour_driver_buckets) + .where( + or( + eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_hour_driver_buckets.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(cost_insight_owner_hour_totals) + .where( + or( + eq(cost_insight_owner_hour_totals.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_hour_totals.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx + .delete(cost_insight_rollup_coverage) + .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); + + for (const user of seedUsers) { + await tx + .insert(kilocode_users) + .values({ + id: user.id, google_user_email: user.email, google_user_name: user.name, google_user_image_url: `https://example.com/dev-seed/${user.id}.png`, + stripe_customer_id: user.stripeCustomerId, normalized_email: user.email, has_validation_stytch: true, customer_source: 'dev-seed', + is_admin: user.isAdmin, microdollars_used: 0, total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, - }, - }); - } + }) + .onConflictDoUpdate({ + target: kilocode_users.id, + set: { + google_user_email: user.email, + google_user_name: user.name, + google_user_image_url: `https://example.com/dev-seed/${user.id}.png`, + stripe_customer_id: user.stripeCustomerId, + normalized_email: user.email, + has_validation_stytch: true, + customer_source: 'dev-seed', + is_admin: user.isAdmin, + microdollars_used: 0, + total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, + }, + }); + } - await tx - .insert(organizations) - .values({ - id: ORGANIZATION_ID, - name: ORGANIZATION_NAME, - created_by_kilo_user_id: PERSONAL_OWNER_ID, - plan: 'teams', - seat_count: 3, - require_seats: true, - microdollars_used: 0, - microdollars_balance: BALANCE_BUFFER_MICRODOLLARS, - total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, - }) - .onConflictDoUpdate({ - target: organizations.id, - set: { + await tx + .insert(organizations) + .values({ + id: ORGANIZATION_ID, name: ORGANIZATION_NAME, created_by_kilo_user_id: PERSONAL_OWNER_ID, plan: 'teams', seat_count: 3, require_seats: true, - deleted_at: null, microdollars_used: 0, microdollars_balance: BALANCE_BUFFER_MICRODOLLARS, total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, + }) + .onConflictDoUpdate({ + target: organizations.id, + set: { + name: ORGANIZATION_NAME, + created_by_kilo_user_id: PERSONAL_OWNER_ID, + plan: 'teams', + seat_count: 3, + require_seats: true, + deleted_at: null, + microdollars_used: 0, + microdollars_balance: BALANCE_BUFFER_MICRODOLLARS, + total_microdollars_acquired: BALANCE_BUFFER_MICRODOLLARS, + }, + }); + + const memberships = [ + { + organization_id: ORGANIZATION_ID, + kilo_user_id: PERSONAL_OWNER_ID, + role: 'owner', }, - }); + { + organization_id: ORGANIZATION_ID, + kilo_user_id: BILLING_MANAGER_ID, + role: 'billing_manager', + }, + { + organization_id: ORGANIZATION_ID, + kilo_user_id: ORGANIZATION_MEMBER_ID, + role: 'member', + }, + ] satisfies (typeof organization_memberships.$inferInsert)[]; + + for (const membership of memberships) { + await tx + .insert(organization_memberships) + .values(membership) + .onConflictDoUpdate({ + target: [ + organization_memberships.organization_id, + organization_memberships.kilo_user_id, + ], + set: { role: membership.role }, + }); + } - const memberships = [ - { - organization_id: ORGANIZATION_ID, - kilo_user_id: PERSONAL_OWNER_ID, - role: 'owner', - }, - { - organization_id: ORGANIZATION_ID, - kilo_user_id: BILLING_MANAGER_ID, - role: 'billing_manager', - }, - { - organization_id: ORGANIZATION_ID, - kilo_user_id: ORGANIZATION_MEMBER_ID, - role: 'member', - }, - ] satisfies (typeof organization_memberships.$inferInsert)[]; + await tx + .insert(feature) + .values(featureKeys.map(featureKey => ({ feature: featureKey }))) + .onConflictDoNothing(); + await tx + .insert(api_kind) + .values(apiKinds.map(apiKind => ({ api_kind: apiKind }))) + .onConflictDoNothing(); + + const featureRows = await tx + .select({ id: feature.feature_id, value: feature.feature }) + .from(feature) + .where(inArray(feature.feature, featureKeys)); + const apiKindRows = await tx + .select({ id: api_kind.api_kind_id, value: api_kind.api_kind }) + .from(api_kind) + .where(inArray(api_kind.api_kind, apiKinds)); + const featureIds = new Map(featureRows.map(row => [row.value, row.id])); + const apiKindIds = new Map(apiKindRows.map(row => [row.value, row.id])); + + const preparedVariableEvents = variableEvents.map((event, index) => { + const id = randomUUID(); + return { + event, + usage: { + id, + kilo_user_id: event.actorUserId, + organization_id: ownerColumns(event.owner).organizationId, + cost: event.amountMicrodollars, + input_tokens: 2_000 + (index % 8) * 750, + output_tokens: 800 + (index % 5) * 450, + cache_write_tokens: index % 3 === 0 ? 400 : 0, + cache_hit_tokens: index % 2 === 0 ? 1_200 : 0, + created_at: event.occurredAt, + provider: event.providerKey, + model: event.modelKey, + requested_model: event.modelKey, + inference_provider: event.providerKey, + has_error: false, + abuse_classification: 0, + } satisfies typeof microdollar_usage.$inferInsert, + metadata: { + id, + created_at: event.occurredAt, + message_id: `${CREDIT_CATEGORY_PREFIX}:usage:${index}`, + feature_id: requireLookupId(featureIds, event.featureKey, 'feature'), + api_kind_id: requireLookupId(apiKindIds, event.apiKind, 'API kind'), + streamed: index % 2 === 0, + is_byok: false, + is_user_byok: false, + has_tools: true, + } satisfies typeof microdollar_usage_metadata.$inferInsert, + }; + }); - for (const membership of memberships) { + await tx.insert(microdollar_usage).values(preparedVariableEvents.map(item => item.usage)); await tx - .insert(organization_memberships) - .values(membership) - .onConflictDoUpdate({ - target: [organization_memberships.organization_id, organization_memberships.kilo_user_id], - set: { role: membership.role }, + .insert(microdollar_usage_metadata) + .values(preparedVariableEvents.map(item => item.metadata)); + + for (const event of variableEvents) { + await captureCostInsightSpend(tx, { + owner: event.owner, + actorUserId: event.actorUserId, + occurredAt: event.occurredAt, + amountMicrodollars: event.amountMicrodollars, + category: 'variable', + source: 'ai_gateway', + productKey: event.featureKey, + featureKey: event.apiKind, + modelOrPlanKey: event.modelKey, + providerKey: event.providerKey, }); - } - - await tx - .insert(feature) - .values(featureKeys.map(featureKey => ({ feature: featureKey }))) - .onConflictDoNothing(); - await tx - .insert(api_kind) - .values(apiKinds.map(apiKind => ({ api_kind: apiKind }))) - .onConflictDoNothing(); - - const featureRows = await tx - .select({ id: feature.feature_id, value: feature.feature }) - .from(feature) - .where(inArray(feature.feature, featureKeys)); - const apiKindRows = await tx - .select({ id: api_kind.api_kind_id, value: api_kind.api_kind }) - .from(api_kind) - .where(inArray(api_kind.api_kind, apiKinds)); - const featureIds = new Map(featureRows.map(row => [row.value, row.id])); - const apiKindIds = new Map(apiKindRows.map(row => [row.value, row.id])); - - const preparedVariableEvents = variableEvents.map((event, index) => { - const id = randomUUID(); - return { - event, - usage: { - id, + } + + const rawOnlyVariableEvents = [ + ...unattributedVariableEvents, + ...(includesLateArrival ? [lateArrivalEvent] : []), + ]; + await tx.insert(microdollar_usage).values( + rawOnlyVariableEvents.map(event => ({ + id: randomUUID(), kilo_user_id: event.actorUserId, organization_id: ownerColumns(event.owner).organizationId, cost: event.amountMicrodollars, - input_tokens: 2_000 + (index % 8) * 750, - output_tokens: 800 + (index % 5) * 450, - cache_write_tokens: index % 3 === 0 ? 400 : 0, - cache_hit_tokens: index % 2 === 0 ? 1_200 : 0, + input_tokens: 1_500, + output_tokens: 600, + cache_write_tokens: 0, + cache_hit_tokens: 0, created_at: event.occurredAt, provider: event.providerKey, model: event.modelKey, @@ -1028,147 +1383,354 @@ export async function run(...args: string[]): Promise { inference_provider: event.providerKey, has_error: false, abuse_classification: 0, - } satisfies typeof microdollar_usage.$inferInsert, - metadata: { - id, - created_at: event.occurredAt, - message_id: `${CREDIT_CATEGORY_PREFIX}:usage:${index}`, - feature_id: requireLookupId(featureIds, event.featureKey, 'feature'), - api_kind_id: requireLookupId(apiKindIds, event.apiKind, 'API kind'), - streamed: index % 2 === 0, + })) satisfies (typeof microdollar_usage.$inferInsert)[] + ); + for (const event of unattributedVariableEvents) { + await captureCostInsightSpend(tx, { + owner: event.owner, + actorUserId: event.actorUserId, + occurredAt: event.occurredAt, + amountMicrodollars: event.amountMicrodollars, + category: 'variable', + source: 'ai_gateway', + productKey: COST_INSIGHT_DRIVER_FALLBACK, + featureKey: COST_INSIGHT_DRIVER_FALLBACK, + modelOrPlanKey: event.modelKey, + providerKey: event.providerKey, + }); + } + + if (includesUnknownTaxonomy) { + const unknownUsageId = randomUUID(); + await tx.insert(microdollar_usage).values({ + id: unknownUsageId, + kilo_user_id: unknownTaxonomyEvent.actorUserId, + organization_id: ownerColumns(unknownTaxonomyEvent.owner).organizationId, + cost: unknownTaxonomyEvent.amountMicrodollars, + input_tokens: 1_800, + output_tokens: 700, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: unknownTaxonomyEvent.occurredAt, + provider: unknownTaxonomyEvent.providerKey, + model: unknownTaxonomyEvent.modelKey, + requested_model: unknownTaxonomyEvent.modelKey, + inference_provider: unknownTaxonomyEvent.providerKey, + has_error: false, + abuse_classification: 0, + }); + await tx.insert(microdollar_usage_metadata).values({ + id: unknownUsageId, + created_at: unknownTaxonomyEvent.occurredAt, + message_id: `${CREDIT_CATEGORY_PREFIX}:unknown-taxonomy`, + feature_id: requireLookupId(featureIds, UNKNOWN_FEATURE_KEY, 'feature'), + api_kind_id: requireLookupId(apiKindIds, unknownTaxonomyEvent.apiKind, 'API kind'), + streamed: false, is_byok: false, is_user_byok: false, has_tools: true, - } satisfies typeof microdollar_usage_metadata.$inferInsert, - }; - }); + }); + await captureCostInsightSpend(tx, { + owner: unknownTaxonomyEvent.owner, + actorUserId: unknownTaxonomyEvent.actorUserId, + occurredAt: unknownTaxonomyEvent.occurredAt, + amountMicrodollars: unknownTaxonomyEvent.amountMicrodollars, + category: 'variable', + source: 'ai_gateway', + productKey: COST_INSIGHT_DRIVER_FALLBACK, + featureKey: unknownTaxonomyEvent.apiKind, + modelOrPlanKey: unknownTaxonomyEvent.modelKey, + providerKey: unknownTaxonomyEvent.providerKey, + }); + } - await tx.insert(microdollar_usage).values(preparedVariableEvents.map(item => item.usage)); - await tx - .insert(microdollar_usage_metadata) - .values(preparedVariableEvents.map(item => item.metadata)); - - for (const event of variableEvents) { - await captureCostInsightSpend(tx, { - owner: event.owner, - actorUserId: event.actorUserId, - occurredAt: event.occurredAt, - amountMicrodollars: event.amountMicrodollars, - category: 'variable', - source: 'ai_gateway', - productKey: event.featureKey, - featureKey: event.apiKind, - modelOrPlanKey: event.modelKey, - providerKey: event.providerKey, + await tx.insert(exa_usage_log).values( + exaEvents.map(event => ({ + id: randomUUID(), + kilo_user_id: event.actorUserId, + organization_id: ownerColumns(event.owner).organizationId, + path: event.path, + cost_microdollars: event.amountMicrodollars, + charged_to_balance: true, + feature_id: `${CREDIT_CATEGORY_PREFIX}:exa`, + type: 'cost-insights-seed', + created_at: event.occurredAt, + })) satisfies (typeof exa_usage_log.$inferInsert)[] + ); + for (const event of exaEvents) { + await captureCostInsightSpend(tx, { + owner: event.owner, + actorUserId: event.actorUserId, + occurredAt: event.occurredAt, + amountMicrodollars: event.amountMicrodollars, + category: 'variable', + source: 'other', + productKey: COST_INSIGHT_EXA_PRODUCT_KEY, + featureKey: event.featureKey, + modelOrPlanKey: COST_INSIGHT_DRIVER_FALLBACK, + providerKey: COST_INSIGHT_EXA_PRODUCT_KEY, + }); + } + + const scheduledRows = scheduledEvents.map((event, index) => ({ + id: randomUUID(), + kilo_user_id: event.actorUserId, + organization_id: ownerColumns(event.owner).organizationId, + amount_microdollars: -event.amountMicrodollars, + is_free: false, + description: kiloclawDescription(event), + credit_category: kiloclawCreditCategory(event, index), + created_at: event.occurredAt, + check_category_uniqueness: false, + })) satisfies (typeof credit_transactions.$inferInsert)[]; + + await tx.insert(credit_transactions).values(scheduledRows); + + for (const event of scheduledEvents) { + await captureCostInsightSpend(tx, { + owner: event.owner, + actorUserId: event.actorUserId, + occurredAt: event.occurredAt, + amountMicrodollars: event.amountMicrodollars, + category: 'scheduled', + source: 'kiloclaw', + productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, + featureKey: event.featureKey, + modelOrPlanKey: event.planKey, + providerKey: COST_INSIGHT_DRIVER_FALLBACK, + }); + } + + const preparedCodingPlanEvents = codingPlanEvents.map((event, index) => { + const subscriptionId = randomUUID(); + const transactionId = randomUUID(); + const periodStart = event.occurredAt; + const periodEnd = new Date(Date.parse(periodStart) + 30 * DAY_MS).toISOString(); + return { + event, + subscription: { + id: subscriptionId, + user_id: event.actorUserId, + plan_id: CODING_PLAN_ID, + provider_id: CODING_PLAN_PROVIDER_ID, + status: 'canceled', + cost_microdollars: event.amountMicrodollars, + billing_period_days: 30, + current_period_start: periodStart, + current_period_end: periodEnd, + credit_renewal_at: periodEnd, + canceled_at: currentHourIso, + cancellation_reason: 'user_canceled', + } satisfies typeof coding_plan_subscriptions.$inferInsert, + transaction: { + id: transactionId, + kilo_user_id: event.actorUserId, + organization_id: ownerColumns(event.owner).organizationId, + amount_microdollars: -event.amountMicrodollars, + is_free: false, + description: `Coding Plan ${event.termKind}: MiniMax Token Plan Plus`, + credit_category: `coding-plan:${CREDIT_CATEGORY_PREFIX}:${index}:${event.termKind}`, + check_category_uniqueness: true, + created_at: event.occurredAt, + } satisfies typeof credit_transactions.$inferInsert, + term: { + id: randomUUID(), + subscription_id: subscriptionId, + user_id: event.actorUserId, + plan_id: CODING_PLAN_ID, + kind: event.termKind, + idempotency_key: `${CREDIT_CATEGORY_PREFIX}:coding-plan:${index}:${event.termKind}`, + period_start: periodStart, + period_end: periodEnd, + cost_microdollars: event.amountMicrodollars, + credit_transaction_id: transactionId, + } satisfies typeof coding_plan_terms.$inferInsert, + }; }); - } + await tx + .insert(coding_plan_subscriptions) + .values(preparedCodingPlanEvents.map(item => item.subscription)); + await tx + .insert(credit_transactions) + .values(preparedCodingPlanEvents.map(item => item.transaction)); + await tx.insert(coding_plan_terms).values(preparedCodingPlanEvents.map(item => item.term)); + for (const event of codingPlanEvents) { + await captureCostInsightSpend(tx, { + owner: event.owner, + actorUserId: event.actorUserId, + occurredAt: event.occurredAt, + amountMicrodollars: event.amountMicrodollars, + category: 'scheduled', + source: 'coding_plan', + productKey: COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, + featureKey: event.termKind, + modelOrPlanKey: CODING_PLAN_ID, + providerKey: CODING_PLAN_PROVIDER_ID, + }); + } - const scheduledRows = scheduledEvents.map((event, index) => ({ - id: randomUUID(), - kilo_user_id: event.actorUserId, - organization_id: ownerColumns(event.owner).organizationId, - amount_microdollars: -event.amountMicrodollars, - is_free: false, - description: kiloclawDescription(event), - credit_category: kiloclawCreditCategory(event, index), - created_at: event.occurredAt, - check_category_uniqueness: false, - })) satisfies (typeof credit_transactions.$inferInsert)[]; - - await tx.insert(credit_transactions).values(scheduledRows); - - for (const event of scheduledEvents) { - await captureCostInsightSpend(tx, { - owner: event.owner, - actorUserId: event.actorUserId, - occurredAt: event.occurredAt, - amountMicrodollars: event.amountMicrodollars, - category: 'scheduled', - source: 'kiloclaw', - productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, - featureKey: event.featureKey, - modelOrPlanKey: event.planKey, - providerKey: COST_INSIGHT_DRIVER_FALLBACK, - }); - } + const personalSpendMicrodollars = + personalVariableMicrodollars + personalScheduledMicrodollars; + const organizationSpendMicrodollars = + organizationVariableMicrodollars + organizationScheduledMicrodollars; + + await tx + .update(kilocode_users) + .set({ + microdollars_used: personalSpendMicrodollars, + total_microdollars_acquired: personalSpendMicrodollars + BALANCE_BUFFER_MICRODOLLARS, + }) + .where(eq(kilocode_users.id, PERSONAL_OWNER_ID)); + await tx + .update(organizations) + .set({ + microdollars_used: organizationSpendMicrodollars, + microdollars_balance: BALANCE_BUFFER_MICRODOLLARS, + total_microdollars_acquired: organizationSpendMicrodollars + BALANCE_BUFFER_MICRODOLLARS, + }) + .where(eq(organizations.id, ORGANIZATION_ID)); + + if (rollupMode === 'bootstrap') { + const completedSeedDrivers = and( + lt(cost_insight_owner_hour_driver_buckets.hour_start, currentHourIso), + or( + eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_hour_driver_buckets.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + const completedSeedTotals = and( + lt(cost_insight_owner_hour_totals.hour_start, currentHourIso), + or( + eq(cost_insight_owner_hour_totals.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_hour_totals.owned_by_organization_id, ORGANIZATION_ID) + ) + ); + await tx.delete(cost_insight_owner_hour_driver_buckets).where(completedSeedDrivers); + await tx.delete(cost_insight_owner_hour_totals).where(completedSeedTotals); + } + + if (rollupMode === 'repairable-drift') { + const missingRollupHour = timestampAtHourOffset(currentHour, 2); + await tx + .delete(cost_insight_owner_hour_driver_buckets) + .where( + and( + eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_hour_driver_buckets.hour_start, missingRollupHour), + eq(cost_insight_owner_hour_driver_buckets.spend_category, 'variable') + ) + ); + await tx + .delete(cost_insight_owner_hour_totals) + .where( + and( + eq(cost_insight_owner_hour_totals.owned_by_user_id, PERSONAL_OWNER_ID), + eq(cost_insight_owner_hour_totals.hour_start, missingRollupHour), + eq(cost_insight_owner_hour_totals.spend_category, 'variable') + ) + ); + } + + if (rollupMode === 'bootstrap' || rollupMode === 'repairable-drift') { + await captureCostInsightSpend(tx, { + owner: ORGANIZATION_OWNER, + actorUserId: BILLING_MANAGER_ID, + occurredAt: staleRollupHourIso, + amountMicrodollars: 880_000, + category: 'variable', + source: 'ai_gateway', + productKey: COST_INSIGHT_DRIVER_FALLBACK, + featureKey: COST_INSIGHT_DRIVER_FALLBACK, + modelOrPlanKey: 'stale-rollup-only', + providerKey: 'dev-seed', + }); + } - const personalSpendMicrodollars = personalVariableMicrodollars + personalScheduledMicrodollars; - const organizationSpendMicrodollars = - organizationVariableMicrodollars + organizationScheduledMicrodollars; - - await tx - .update(kilocode_users) - .set({ - microdollars_used: personalSpendMicrodollars, - total_microdollars_acquired: personalSpendMicrodollars + BALANCE_BUFFER_MICRODOLLARS, - }) - .where(eq(kilocode_users.id, PERSONAL_OWNER_ID)); - await tx - .update(organizations) - .set({ - microdollars_used: organizationSpendMicrodollars, - microdollars_balance: BALANCE_BUFFER_MICRODOLLARS, - total_microdollars_acquired: organizationSpendMicrodollars + BALANCE_BUFFER_MICRODOLLARS, - }) - .where(eq(organizations.id, ORGANIZATION_ID)); - - await tx - .insert(cost_insight_rollup_coverage) - .values({ + await tx.insert(cost_insight_rollup_coverage).values({ rollup_version: 1, live_capture_start_hour: currentHourIso, - coverage_start_hour: coverageStartIso, - }) - .onConflictDoUpdate({ - target: cost_insight_rollup_coverage.rollup_version, - set: { - live_capture_start_hour: sql`COALESCE(${cost_insight_rollup_coverage.live_capture_start_hour}, ${currentHourIso})`, - coverage_start_hour: sql`LEAST( - COALESCE(${cost_insight_rollup_coverage.coverage_start_hour}, ${coverageStartIso}), - ${coverageStartIso}, - COALESCE(${cost_insight_rollup_coverage.live_capture_start_hour}, ${currentHourIso}) - )`, - updated_at: sql`CURRENT_TIMESTAMP`, - }, + coverage_start_hour: rollupMode === 'bootstrap' ? currentHourIso : coverageStartIso, }); - await tx.insert(cost_insight_owner_configs).values([ - { - ...costInsightOwnerColumns(PERSONAL_OWNER), - spend_alerts_enabled: true, - cost_suggestions_enabled: true, - spend_threshold_microdollars: personalThresholdMicrodollars, - spend_alerts_enabled_at: timestampAtHourOffset(currentHour, 18), - }, - { - ...costInsightOwnerColumns(ORGANIZATION_OWNER), - spend_alerts_enabled: true, - cost_suggestions_enabled: true, - spend_threshold_microdollars: organizationThresholdMicrodollars, - spend_alerts_enabled_at: timestampAtHourOffset(currentHour, 18), - }, - ]); - await tx.insert(cost_insight_active_suggestions).values(costInsightSuggestionRows); - await tx.insert(cost_insight_events).values(costInsightEventRows); - await tx.insert(cost_insight_owner_states).values(costInsightStateRows); - await tx.insert(cost_insight_notification_deliveries).values(costInsightNotificationRows); - }); + if (rollupMode === 'degraded-late') { + await tx.insert(cost_insight_rollup_degraded_intervals).values({ + id: DEGRADED_INTERVAL_ID, + start_hour: lateArrivalHourIso, + end_hour_exclusive: new Date(Date.parse(lateArrivalHourIso) + HOUR_MS).toISOString(), + source: 'ai_gateway', + reason: 'late_source_data', + }); + } + + await tx.insert(cost_insight_owner_configs).values([ + { + ...costInsightOwnerColumns(PERSONAL_OWNER), + spend_alerts_enabled: true, + cost_suggestions_enabled: true, + spend_threshold_microdollars: personalThresholdMicrodollars, + spend_alerts_enabled_at: timestampAtHourOffset(currentHour, 18), + }, + { + ...costInsightOwnerColumns(ORGANIZATION_OWNER), + spend_alerts_enabled: true, + cost_suggestions_enabled: true, + spend_threshold_microdollars: organizationThresholdMicrodollars, + spend_alerts_enabled_at: timestampAtHourOffset(currentHour, 18), + }, + ]); + await tx.insert(cost_insight_active_suggestions).values(costInsightSuggestionRows); + await tx.insert(cost_insight_events).values(costInsightEventRows); + await tx.insert(cost_insight_owner_states).values(costInsightStateRows); + await tx.insert(cost_insight_notification_deliveries).values(costInsightNotificationRows); + }); + } catch (error) { + await Promise.all(seedUsers.map(user => deleteSeedStripeCustomer(user.stripeCustomerId))); + throw error; + } + for (const user of seedUsers) { + const previousStripeCustomerId = previousStripeCustomerIds.get(user.id); + if ( + previousStripeCustomerId && + previousStripeCustomerId !== user.stripeCustomerId && + !previousStripeCustomerId.startsWith('cus_dev_seed_') + ) { + await deleteSeedStripeCustomer(previousStripeCustomerId); + } + } + + const executeSafe = rollupMode !== 'unknown-taxonomy' && rollupMode !== 'degraded-late'; console.log(''); - console.log('This fixture represents:'); + console.log(`This fixture represents (${rollupMode} rollup mode):`); console.log('- 90 days of personal and organization Variable Credit spend.'); - console.log('- Monthly KiloClaw Scheduled Credit spend.'); - console.log('- Current-hour anomaly spikes and rolling 24-hour threshold crossings.'); - console.log('- Active Spend Alert banners, Cost Suggestions, notification rows, and activity.'); - console.log('- Three organization members contributing distinct top spend drivers.'); + console.log('- AI Gateway, Exa, Coding Plan, and KiloClaw canonical source records.'); + console.log('- Missing AI Gateway metadata with controlled fallback driver dimensions.'); + console.log('- Current-hour anomalies, Spend Alerts, Cost Suggestions, and activity history.'); + if (rollupMode === 'bootstrap') { + console.log( + '- Current-hour live capture plus missing history, late data, and one stale rollup.' + ); + } else if (rollupMode === 'repairable-drift') { + console.log('- Complete coverage with one missing rollup, late record, and stale rollup.'); + } else if (rollupMode === 'unknown-taxonomy') { + console.log('- One unknown AI Gateway product taxonomy value for dry-run diagnostics.'); + } else if (rollupMode === 'degraded-late') { + console.log('- One late source row inside an unresolved degraded interval.'); + } else { + console.log('- Matching hourly rollups with complete 90-day coverage.'); + } console.log(''); - console.log('Seed users are DB-only Cost Insights fixtures with placeholder Stripe IDs.'); - console.log('Use development fake login; avoid Stripe-backed billing pages with these users.'); + console.log('Seed users have real Stripe test customers and support Stripe-backed pages.'); + console.log('Use development fake login to open personal or organization Cost Insights.'); return { databaseTarget: `${databaseTarget.hostname}:${databaseTarget.port}/${databaseTarget.database}`, + rollupMode, + executeSafe, personalOwnerId: PERSONAL_OWNER_ID, personalOwnerEmail: PERSONAL_OWNER_EMAIL, + personalStripeCustomerId: + seedUsers.find(user => user.id === PERSONAL_OWNER_ID)?.stripeCustomerId ?? null, personalPath: '/cost-insights', personalLoginPath: loginPath(PERSONAL_OWNER_EMAIL, '/cost-insights'), organizationId: ORGANIZATION_ID, @@ -1180,12 +1742,32 @@ export async function run(...args: string[]): Promise { ), billingManagerId: BILLING_MANAGER_ID, billingManagerEmail: BILLING_MANAGER_EMAIL, + billingManagerStripeCustomerId: + seedUsers.find(user => user.id === BILLING_MANAGER_ID)?.stripeCustomerId ?? null, organizationMemberId: ORGANIZATION_MEMBER_ID, organizationMemberEmail: ORGANIZATION_MEMBER_EMAIL, + organizationMemberStripeCustomerId: + seedUsers.find(user => user.id === ORGANIZATION_MEMBER_ID)?.stripeCustomerId ?? null, coverageStartHour: coverageStartIso, + rollupCoverageStartHour: rollupMode === 'bootstrap' ? currentHourIso : coverageStartIso, currentHour: currentHourIso, - variableRecordCount: variableEvents.length, - scheduledRecordCount: scheduledEvents.length, + maintenanceStartHour: rollupMode === 'bootstrap' ? coverageStartIso : maintenanceStartIso, + maintenanceEndHour: currentHourIso, + lateArrivalHour: includesLateArrival ? lateArrivalHourIso : null, + staleRollupHour: + rollupMode === 'bootstrap' || rollupMode === 'repairable-drift' ? staleRollupHourIso : null, + aiGatewayRecordCount: + variableEvents.length + + unattributedVariableEvents.length + + (includesLateArrival ? 1 : 0) + + (includesUnknownTaxonomy ? 1 : 0), + exaRecordCount: exaEvents.length, + codingPlanRecordCount: codingPlanEvents.length, + kiloclawRecordCount: scheduledEvents.length, + variableRecordCount: canonicalVariableSpendEvents.length, + scheduledRecordCount: canonicalScheduledSpendEvents.length, + missingMetadataRecordCount: unattributedVariableEvents.length, + unknownTaxonomyRecordCount: includesUnknownTaxonomy ? 1 : 0, personalVariableMicrodollars, personalScheduledMicrodollars, organizationVariableMicrodollars, diff --git a/packages/db/src/migrations/0174_sleepy_virginia_dare.sql b/packages/db/src/migrations/0174_sleepy_virginia_dare.sql new file mode 100644 index 0000000000..e2a01edb36 --- /dev/null +++ b/packages/db/src/migrations/0174_sleepy_virginia_dare.sql @@ -0,0 +1,257 @@ +CREATE TABLE "cost_insight_active_suggestions" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "owned_by_user_id" text, + "owned_by_organization_id" uuid, + "suggestion_kind" text NOT NULL, + "suggestion_key" text NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "cta_label" text NOT NULL, + "cta_href" text NOT NULL, + "evidence_window_start" timestamp with time zone NOT NULL, + "evidence_window_end" timestamp with time zone NOT NULL, + "observed_microdollars" bigint NOT NULL, + "benefit_label" text NOT NULL, + "benefit_detail" text NOT NULL, + "dismissed_at" timestamp with time zone, + "dismissed_by_user_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_active_suggestions_owner_check" CHECK (("cost_insight_active_suggestions"."owned_by_user_id" IS NOT NULL AND "cost_insight_active_suggestions"."owned_by_organization_id" IS NULL) OR ("cost_insight_active_suggestions"."owned_by_user_id" IS NULL AND "cost_insight_active_suggestions"."owned_by_organization_id" IS NOT NULL)), + CONSTRAINT "cost_insight_active_suggestions_kind_check" CHECK ("cost_insight_active_suggestions"."suggestion_kind" IN ('coding_plan', 'kilo_pass')), + CONSTRAINT "cost_insight_active_suggestions_key_check" CHECK ("cost_insight_active_suggestions"."suggestion_key" ~ '^[0-9a-f]{64}$'), + CONSTRAINT "cost_insight_active_suggestions_window_check" CHECK ("cost_insight_active_suggestions"."evidence_window_end" > "cost_insight_active_suggestions"."evidence_window_start"), + CONSTRAINT "cost_insight_active_suggestions_observed_positive_check" CHECK ("cost_insight_active_suggestions"."observed_microdollars" > 0), + CONSTRAINT "cost_insight_active_suggestions_observed_safe_check" CHECK ("cost_insight_active_suggestions"."observed_microdollars" <= 9007199254740991), + CONSTRAINT "cost_insight_active_suggestions_dismissed_by_check" CHECK ("cost_insight_active_suggestions"."dismissed_at" IS NOT NULL OR "cost_insight_active_suggestions"."dismissed_by_user_id" IS NULL) +); +--> statement-breakpoint +CREATE TABLE "cost_insight_events" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "owned_by_user_id" text, + "owned_by_organization_id" uuid, + "event_type" text NOT NULL, + "alert_kind" text, + "suggestion_kind" text, + "active_suggestion_id" uuid, + "actor_user_id" text, + "title" text NOT NULL, + "description" text NOT NULL, + "snapshot" jsonb DEFAULT '{}'::jsonb NOT NULL, + "dedupe_key" text, + "occurred_at" timestamp with time zone DEFAULT now() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_events_owner_check" CHECK (("cost_insight_events"."owned_by_user_id" IS NOT NULL AND "cost_insight_events"."owned_by_organization_id" IS NULL) OR ("cost_insight_events"."owned_by_user_id" IS NULL AND "cost_insight_events"."owned_by_organization_id" IS NOT NULL)), + CONSTRAINT "cost_insight_events_type_check" CHECK ("cost_insight_events"."event_type" IN ('config_changed', 'anomaly_alert', 'threshold_crossed', 'alert_reviewed', 'suggestion_created', 'suggestion_dismissed', 'disabled')), + CONSTRAINT "cost_insight_events_alert_kind_check" CHECK ("cost_insight_events"."alert_kind" IN ('anomaly', 'threshold', 'threshold_7d', 'threshold_30d')), + CONSTRAINT "cost_insight_events_suggestion_kind_check" CHECK ("cost_insight_events"."suggestion_kind" IN ('coding_plan', 'kilo_pass')), + CONSTRAINT "cost_insight_events_alert_kind_presence_check" CHECK (("cost_insight_events"."event_type" IN ('anomaly_alert', 'threshold_crossed', 'alert_reviewed') AND "cost_insight_events"."alert_kind" IS NOT NULL) OR ("cost_insight_events"."event_type" NOT IN ('anomaly_alert', 'threshold_crossed', 'alert_reviewed') AND "cost_insight_events"."alert_kind" IS NULL)), + CONSTRAINT "cost_insight_events_suggestion_kind_presence_check" CHECK (("cost_insight_events"."event_type" IN ('suggestion_created', 'suggestion_dismissed') AND "cost_insight_events"."suggestion_kind" IS NOT NULL) OR ("cost_insight_events"."event_type" NOT IN ('suggestion_created', 'suggestion_dismissed') AND "cost_insight_events"."suggestion_kind" IS NULL)) +); +--> statement-breakpoint +CREATE TABLE "cost_insight_notification_deliveries" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "event_id" uuid NOT NULL, + "recipient_user_id" text NOT NULL, + "channel" text DEFAULT 'email' NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "attempt_count" integer DEFAULT 0 NOT NULL, + "next_attempt_at" timestamp with time zone DEFAULT now() NOT NULL, + "claimed_at" timestamp with time zone, + "sent_at" timestamp with time zone, + "failed_at" timestamp with time zone, + "last_error_redacted" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_notification_deliveries_channel_check" CHECK ("cost_insight_notification_deliveries"."channel" = 'email'), + CONSTRAINT "cost_insight_notification_deliveries_status_check" CHECK ("cost_insight_notification_deliveries"."status" IN ('pending', 'sending', 'sent', 'failed', 'skipped')), + CONSTRAINT "cost_insight_notification_deliveries_attempt_count_check" CHECK ("cost_insight_notification_deliveries"."attempt_count" >= 0), + CONSTRAINT "cost_insight_notification_deliveries_terminal_check" CHECK (("cost_insight_notification_deliveries"."status" = 'sent' AND "cost_insight_notification_deliveries"."sent_at" IS NOT NULL) OR ("cost_insight_notification_deliveries"."status" <> 'sent' AND "cost_insight_notification_deliveries"."sent_at" IS NULL)), + CONSTRAINT "cost_insight_notification_deliveries_failure_check" CHECK (("cost_insight_notification_deliveries"."status" = 'failed' AND "cost_insight_notification_deliveries"."failed_at" IS NOT NULL) OR ("cost_insight_notification_deliveries"."status" <> 'failed' AND "cost_insight_notification_deliveries"."failed_at" IS NULL)) +); +--> statement-breakpoint +CREATE TABLE "cost_insight_owner_configs" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "owned_by_user_id" text, + "owned_by_organization_id" uuid, + "spend_alerts_enabled" boolean DEFAULT false NOT NULL, + "anomaly_alerts_enabled" boolean DEFAULT true NOT NULL, + "cost_suggestions_enabled" boolean DEFAULT true NOT NULL, + "spend_threshold_microdollars" bigint, + "spend_7_day_threshold_microdollars" bigint, + "spend_30_day_threshold_microdollars" bigint, + "spend_alerts_enabled_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_owner_configs_owner_check" CHECK (("cost_insight_owner_configs"."owned_by_user_id" IS NOT NULL AND "cost_insight_owner_configs"."owned_by_organization_id" IS NULL) OR ("cost_insight_owner_configs"."owned_by_user_id" IS NULL AND "cost_insight_owner_configs"."owned_by_organization_id" IS NOT NULL)), + CONSTRAINT "cost_insight_owner_configs_threshold_positive_check" CHECK ("cost_insight_owner_configs"."spend_threshold_microdollars" IS NULL OR "cost_insight_owner_configs"."spend_threshold_microdollars" > 0), + CONSTRAINT "cost_insight_owner_configs_threshold_safe_check" CHECK ("cost_insight_owner_configs"."spend_threshold_microdollars" IS NULL OR "cost_insight_owner_configs"."spend_threshold_microdollars" <= 9007199254740991), + CONSTRAINT "cost_insight_owner_configs_7_day_threshold_positive_check" CHECK ("cost_insight_owner_configs"."spend_7_day_threshold_microdollars" IS NULL OR "cost_insight_owner_configs"."spend_7_day_threshold_microdollars" > 0), + CONSTRAINT "cost_insight_owner_configs_7_day_threshold_safe_check" CHECK ("cost_insight_owner_configs"."spend_7_day_threshold_microdollars" IS NULL OR "cost_insight_owner_configs"."spend_7_day_threshold_microdollars" <= 9007199254740991), + CONSTRAINT "cost_insight_owner_configs_30_day_threshold_positive_check" CHECK ("cost_insight_owner_configs"."spend_30_day_threshold_microdollars" IS NULL OR "cost_insight_owner_configs"."spend_30_day_threshold_microdollars" > 0), + CONSTRAINT "cost_insight_owner_configs_30_day_threshold_safe_check" CHECK ("cost_insight_owner_configs"."spend_30_day_threshold_microdollars" IS NULL OR "cost_insight_owner_configs"."spend_30_day_threshold_microdollars" <= 9007199254740991), + CONSTRAINT "cost_insight_owner_configs_enabled_at_check" CHECK ("cost_insight_owner_configs"."spend_alerts_enabled" = TRUE OR "cost_insight_owner_configs"."spend_alerts_enabled_at" IS NULL) +); +--> statement-breakpoint +CREATE TABLE "cost_insight_owner_hour_driver_buckets" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "owned_by_user_id" text, + "owned_by_organization_id" uuid, + "hour_start" timestamp with time zone NOT NULL, + "spend_category" text NOT NULL, + "driver_key" text NOT NULL, + "source" text NOT NULL, + "product_key" text NOT NULL, + "feature_key" text NOT NULL, + "model_or_plan_key" text NOT NULL, + "provider_key" text NOT NULL, + "actor_user_id" text NOT NULL, + "total_microdollars" bigint NOT NULL, + "spend_record_count" bigint NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_driver_buckets_owner_check" CHECK (("cost_insight_owner_hour_driver_buckets"."owned_by_user_id" IS NOT NULL AND "cost_insight_owner_hour_driver_buckets"."owned_by_organization_id" IS NULL) OR ("cost_insight_owner_hour_driver_buckets"."owned_by_user_id" IS NULL AND "cost_insight_owner_hour_driver_buckets"."owned_by_organization_id" IS NOT NULL)), + CONSTRAINT "cost_insight_driver_buckets_hour_check" CHECK ("cost_insight_owner_hour_driver_buckets"."hour_start" = date_trunc('hour', "cost_insight_owner_hour_driver_buckets"."hour_start", 'UTC')), + CONSTRAINT "cost_insight_driver_buckets_category_check" CHECK ("cost_insight_owner_hour_driver_buckets"."spend_category" IN ('variable', 'scheduled')), + CONSTRAINT "cost_insight_driver_buckets_source_check" CHECK ("cost_insight_owner_hour_driver_buckets"."source" IN ('ai_gateway', 'kiloclaw', 'coding_plan', 'other')), + CONSTRAINT "cost_insight_driver_buckets_driver_key_check" CHECK ("cost_insight_owner_hour_driver_buckets"."driver_key" ~ '^[0-9a-f]{64}$'), + CONSTRAINT "cost_insight_driver_buckets_product_key_check" CHECK (char_length("cost_insight_owner_hour_driver_buckets"."product_key") BETWEEN 1 AND 128), + CONSTRAINT "cost_insight_driver_buckets_feature_key_check" CHECK (char_length("cost_insight_owner_hour_driver_buckets"."feature_key") BETWEEN 1 AND 128), + CONSTRAINT "cost_insight_driver_buckets_model_key_check" CHECK (char_length("cost_insight_owner_hour_driver_buckets"."model_or_plan_key") BETWEEN 1 AND 128), + CONSTRAINT "cost_insight_driver_buckets_provider_key_check" CHECK (char_length("cost_insight_owner_hour_driver_buckets"."provider_key") BETWEEN 1 AND 128), + CONSTRAINT "cost_insight_driver_buckets_amount_positive_check" CHECK ("cost_insight_owner_hour_driver_buckets"."total_microdollars" > 0), + CONSTRAINT "cost_insight_driver_buckets_amount_safe_check" CHECK ("cost_insight_owner_hour_driver_buckets"."total_microdollars" <= 9007199254740991), + CONSTRAINT "cost_insight_driver_buckets_count_positive_check" CHECK ("cost_insight_owner_hour_driver_buckets"."spend_record_count" > 0), + CONSTRAINT "cost_insight_driver_buckets_count_safe_check" CHECK ("cost_insight_owner_hour_driver_buckets"."spend_record_count" <= 9007199254740991) +); +--> statement-breakpoint +CREATE TABLE "cost_insight_owner_hour_totals" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "owned_by_user_id" text, + "owned_by_organization_id" uuid, + "hour_start" timestamp with time zone NOT NULL, + "spend_category" text NOT NULL, + "total_microdollars" bigint NOT NULL, + "spend_record_count" bigint NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_owner_hour_totals_owner_check" CHECK (("cost_insight_owner_hour_totals"."owned_by_user_id" IS NOT NULL AND "cost_insight_owner_hour_totals"."owned_by_organization_id" IS NULL) OR ("cost_insight_owner_hour_totals"."owned_by_user_id" IS NULL AND "cost_insight_owner_hour_totals"."owned_by_organization_id" IS NOT NULL)), + CONSTRAINT "cost_insight_owner_hour_totals_hour_check" CHECK ("cost_insight_owner_hour_totals"."hour_start" = date_trunc('hour', "cost_insight_owner_hour_totals"."hour_start", 'UTC')), + CONSTRAINT "cost_insight_owner_hour_totals_category_check" CHECK ("cost_insight_owner_hour_totals"."spend_category" IN ('variable', 'scheduled')), + CONSTRAINT "cost_insight_owner_hour_totals_amount_positive_check" CHECK ("cost_insight_owner_hour_totals"."total_microdollars" > 0), + CONSTRAINT "cost_insight_owner_hour_totals_amount_safe_check" CHECK ("cost_insight_owner_hour_totals"."total_microdollars" <= 9007199254740991), + CONSTRAINT "cost_insight_owner_hour_totals_count_positive_check" CHECK ("cost_insight_owner_hour_totals"."spend_record_count" > 0), + CONSTRAINT "cost_insight_owner_hour_totals_count_safe_check" CHECK ("cost_insight_owner_hour_totals"."spend_record_count" <= 9007199254740991) +); +--> statement-breakpoint +CREATE TABLE "cost_insight_owner_states" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "owned_by_user_id" text, + "owned_by_organization_id" uuid, + "last_evaluated_at" timestamp with time zone, + "active_anomaly_event_id" uuid, + "active_anomaly_hour_start" timestamp with time zone, + "active_anomaly_reviewed_at" timestamp with time zone, + "threshold_crossing_active" boolean DEFAULT false NOT NULL, + "active_threshold_event_id" uuid, + "threshold_crossing_started_at" timestamp with time zone, + "threshold_reviewed_at" timestamp with time zone, + "threshold_recovered_at" timestamp with time zone, + "rolling_7_day_threshold_crossing_active" boolean DEFAULT false NOT NULL, + "active_rolling_7_day_threshold_event_id" uuid, + "rolling_7_day_threshold_crossing_started_at" timestamp with time zone, + "rolling_7_day_threshold_reviewed_at" timestamp with time zone, + "rolling_7_day_threshold_recovered_at" timestamp with time zone, + "rolling_30_day_threshold_crossing_active" boolean DEFAULT false NOT NULL, + "active_rolling_30_day_threshold_event_id" uuid, + "rolling_30_day_threshold_crossing_started_at" timestamp with time zone, + "rolling_30_day_threshold_reviewed_at" timestamp with time zone, + "rolling_30_day_threshold_recovered_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_owner_states_owner_check" CHECK (("cost_insight_owner_states"."owned_by_user_id" IS NOT NULL AND "cost_insight_owner_states"."owned_by_organization_id" IS NULL) OR ("cost_insight_owner_states"."owned_by_user_id" IS NULL AND "cost_insight_owner_states"."owned_by_organization_id" IS NOT NULL)), + CONSTRAINT "cost_insight_owner_states_anomaly_hour_check" CHECK ("cost_insight_owner_states"."active_anomaly_hour_start" IS NULL OR "cost_insight_owner_states"."active_anomaly_hour_start" = date_trunc('hour', "cost_insight_owner_states"."active_anomaly_hour_start", 'UTC')), + CONSTRAINT "cost_insight_owner_states_threshold_active_check" CHECK ("cost_insight_owner_states"."threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_threshold_event_id" IS NULL AND "cost_insight_owner_states"."threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL)), + CONSTRAINT "cost_insight_owner_states_7_day_threshold_active_check" CHECK ("cost_insight_owner_states"."rolling_7_day_threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_event_id" IS NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL)), + CONSTRAINT "cost_insight_owner_states_30_day_threshold_active_check" CHECK ("cost_insight_owner_states"."rolling_30_day_threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_event_id" IS NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL)) +); +--> statement-breakpoint +CREATE TABLE "cost_insight_rollup_coverage" ( + "rollup_version" smallint PRIMARY KEY NOT NULL, + "live_capture_start_hour" timestamp with time zone, + "coverage_start_hour" timestamp with time zone, + "last_reconciled_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_rollup_coverage_version_check" CHECK ("cost_insight_rollup_coverage"."rollup_version" > 0), + CONSTRAINT "cost_insight_rollup_coverage_live_hour_check" CHECK ("cost_insight_rollup_coverage"."live_capture_start_hour" IS NULL OR "cost_insight_rollup_coverage"."live_capture_start_hour" = date_trunc('hour', "cost_insight_rollup_coverage"."live_capture_start_hour", 'UTC')), + CONSTRAINT "cost_insight_rollup_coverage_start_hour_check" CHECK ("cost_insight_rollup_coverage"."coverage_start_hour" IS NULL OR "cost_insight_rollup_coverage"."coverage_start_hour" = date_trunc('hour', "cost_insight_rollup_coverage"."coverage_start_hour", 'UTC')), + CONSTRAINT "cost_insight_rollup_coverage_range_check" CHECK ("cost_insight_rollup_coverage"."coverage_start_hour" IS NULL OR ("cost_insight_rollup_coverage"."live_capture_start_hour" IS NOT NULL AND "cost_insight_rollup_coverage"."coverage_start_hour" <= "cost_insight_rollup_coverage"."live_capture_start_hour")) +); +--> statement-breakpoint +CREATE TABLE "cost_insight_rollup_degraded_intervals" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "start_hour" timestamp with time zone NOT NULL, + "end_hour_exclusive" timestamp with time zone NOT NULL, + "source" text, + "reason" text NOT NULL, + "detected_at" timestamp with time zone DEFAULT now() NOT NULL, + "resolved_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_degraded_intervals_start_hour_check" CHECK ("cost_insight_rollup_degraded_intervals"."start_hour" = date_trunc('hour', "cost_insight_rollup_degraded_intervals"."start_hour", 'UTC')), + CONSTRAINT "cost_insight_degraded_intervals_end_hour_check" CHECK ("cost_insight_rollup_degraded_intervals"."end_hour_exclusive" = date_trunc('hour', "cost_insight_rollup_degraded_intervals"."end_hour_exclusive", 'UTC')), + CONSTRAINT "cost_insight_degraded_intervals_range_check" CHECK ("cost_insight_rollup_degraded_intervals"."end_hour_exclusive" > "cost_insight_rollup_degraded_intervals"."start_hour"), + CONSTRAINT "cost_insight_degraded_intervals_resolution_check" CHECK ("cost_insight_rollup_degraded_intervals"."resolved_at" IS NULL OR "cost_insight_rollup_degraded_intervals"."resolved_at" >= "cost_insight_rollup_degraded_intervals"."detected_at"), + CONSTRAINT "cost_insight_degraded_intervals_source_check" CHECK ("cost_insight_rollup_degraded_intervals"."source" IN ('ai_gateway', 'kiloclaw', 'coding_plan', 'other')), + CONSTRAINT "cost_insight_degraded_intervals_reason_check" CHECK ("cost_insight_rollup_degraded_intervals"."reason" IN ('capture_bypass', 'reconciliation_mismatch', 'late_source_data')) +); +--> statement-breakpoint +ALTER TABLE "cost_insight_active_suggestions" ADD CONSTRAINT "cost_insight_active_suggestions_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_active_suggestions" ADD CONSTRAINT "cost_insight_active_suggestions_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_active_suggestions" ADD CONSTRAINT "cost_insight_active_suggestions_dismissed_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("dismissed_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_events" ADD CONSTRAINT "cost_insight_events_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_events" ADD CONSTRAINT "cost_insight_events_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_events" ADD CONSTRAINT "cost_insight_events_active_suggestion_id_cost_insight_active_suggestions_id_fk" FOREIGN KEY ("active_suggestion_id") REFERENCES "public"."cost_insight_active_suggestions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_events" ADD CONSTRAINT "cost_insight_events_actor_user_id_kilocode_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_notification_deliveries" ADD CONSTRAINT "cost_insight_notification_deliveries_event_id_cost_insight_events_id_fk" FOREIGN KEY ("event_id") REFERENCES "public"."cost_insight_events"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_notification_deliveries" ADD CONSTRAINT "cost_insight_notification_deliveries_recipient_user_id_kilocode_users_id_fk" FOREIGN KEY ("recipient_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_configs" ADD CONSTRAINT "cost_insight_owner_configs_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_configs" ADD CONSTRAINT "cost_insight_owner_configs_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_hour_driver_buckets" ADD CONSTRAINT "cost_insight_owner_hour_driver_buckets_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_hour_driver_buckets" ADD CONSTRAINT "cost_insight_owner_hour_driver_buckets_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_hour_driver_buckets" ADD CONSTRAINT "cost_insight_owner_hour_driver_buckets_actor_user_id_kilocode_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_hour_totals" ADD CONSTRAINT "cost_insight_owner_hour_totals_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_hour_totals" ADD CONSTRAINT "cost_insight_owner_hour_totals_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_states" ADD CONSTRAINT "cost_insight_owner_states_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_states" ADD CONSTRAINT "cost_insight_owner_states_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_states" ADD CONSTRAINT "cost_insight_owner_states_active_anomaly_event_id_cost_insight_events_id_fk" FOREIGN KEY ("active_anomaly_event_id") REFERENCES "public"."cost_insight_events"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_states" ADD CONSTRAINT "cost_insight_owner_states_active_threshold_event_id_cost_insight_events_id_fk" FOREIGN KEY ("active_threshold_event_id") REFERENCES "public"."cost_insight_events"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_states" ADD CONSTRAINT "cost_insight_owner_states_active_rolling_7_day_threshold_event_id_cost_insight_events_id_fk" FOREIGN KEY ("active_rolling_7_day_threshold_event_id") REFERENCES "public"."cost_insight_events"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_owner_states" ADD CONSTRAINT "cost_insight_owner_states_active_rolling_30_day_threshold_event_id_cost_insight_events_id_fk" FOREIGN KEY ("active_rolling_30_day_threshold_event_id") REFERENCES "public"."cost_insight_events"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_active_suggestions_user_key" ON "cost_insight_active_suggestions" USING btree ("owned_by_user_id","suggestion_key") WHERE "cost_insight_active_suggestions"."owned_by_organization_id" is null;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_active_suggestions_org_key" ON "cost_insight_active_suggestions" USING btree ("owned_by_organization_id","suggestion_key") WHERE "cost_insight_active_suggestions"."owned_by_user_id" is null;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_active_suggestions_user_active" ON "cost_insight_active_suggestions" USING btree ("owned_by_user_id","created_at" DESC NULLS LAST) WHERE "cost_insight_active_suggestions"."owned_by_user_id" IS NOT NULL AND "cost_insight_active_suggestions"."dismissed_at" IS NULL;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_active_suggestions_org_active" ON "cost_insight_active_suggestions" USING btree ("owned_by_organization_id","created_at" DESC NULLS LAST) WHERE "cost_insight_active_suggestions"."owned_by_organization_id" IS NOT NULL AND "cost_insight_active_suggestions"."dismissed_at" IS NULL;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_events_user_occurred" ON "cost_insight_events" USING btree ("owned_by_user_id","occurred_at" DESC NULLS LAST,"id");--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_events_org_occurred" ON "cost_insight_events" USING btree ("owned_by_organization_id","occurred_at" DESC NULLS LAST,"id");--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_events_occurred" ON "cost_insight_events" USING btree ("occurred_at");--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_events_user_dedupe" ON "cost_insight_events" USING btree ("owned_by_user_id","dedupe_key") WHERE "cost_insight_events"."owned_by_user_id" IS NOT NULL AND "cost_insight_events"."dedupe_key" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_events_org_dedupe" ON "cost_insight_events" USING btree ("owned_by_organization_id","dedupe_key") WHERE "cost_insight_events"."owned_by_organization_id" IS NOT NULL AND "cost_insight_events"."dedupe_key" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_notification_deliveries_event_recipient_channel" ON "cost_insight_notification_deliveries" USING btree ("event_id","recipient_user_id","channel");--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_notification_deliveries_claim" ON "cost_insight_notification_deliveries" USING btree ("status","next_attempt_at","id");--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_notification_deliveries_event" ON "cost_insight_notification_deliveries" USING btree ("event_id");--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_owner_configs_user" ON "cost_insight_owner_configs" USING btree ("owned_by_user_id") WHERE "cost_insight_owner_configs"."owned_by_organization_id" is null;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_owner_configs_org" ON "cost_insight_owner_configs" USING btree ("owned_by_organization_id") WHERE "cost_insight_owner_configs"."owned_by_user_id" is null;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_owner_configs_evaluation" ON "cost_insight_owner_configs" USING btree ("updated_at","id") WHERE "cost_insight_owner_configs"."spend_alerts_enabled" = TRUE OR "cost_insight_owner_configs"."cost_suggestions_enabled" = TRUE;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_driver_buckets_user" ON "cost_insight_owner_hour_driver_buckets" USING btree ("owned_by_user_id","hour_start","spend_category","driver_key") WHERE "cost_insight_owner_hour_driver_buckets"."owned_by_organization_id" is null;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_driver_buckets_org" ON "cost_insight_owner_hour_driver_buckets" USING btree ("owned_by_organization_id","hour_start","spend_category","driver_key") WHERE "cost_insight_owner_hour_driver_buckets"."owned_by_user_id" is null;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_driver_buckets_hour" ON "cost_insight_owner_hour_driver_buckets" USING btree ("hour_start");--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_owner_hour_totals_user" ON "cost_insight_owner_hour_totals" USING btree ("owned_by_user_id","hour_start","spend_category") WHERE "cost_insight_owner_hour_totals"."owned_by_organization_id" is null;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_owner_hour_totals_org" ON "cost_insight_owner_hour_totals" USING btree ("owned_by_organization_id","hour_start","spend_category") WHERE "cost_insight_owner_hour_totals"."owned_by_user_id" is null;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_owner_hour_totals_hour" ON "cost_insight_owner_hour_totals" USING btree ("hour_start");--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_owner_states_user" ON "cost_insight_owner_states" USING btree ("owned_by_user_id") WHERE "cost_insight_owner_states"."owned_by_organization_id" is null;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_owner_states_org" ON "cost_insight_owner_states" USING btree ("owned_by_organization_id") WHERE "cost_insight_owner_states"."owned_by_user_id" is null;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_owner_states_unreviewed_user" ON "cost_insight_owner_states" USING btree ("owned_by_user_id","updated_at") WHERE "cost_insight_owner_states"."owned_by_user_id" IS NOT NULL AND (("cost_insight_owner_states"."active_anomaly_event_id" IS NOT NULL AND "cost_insight_owner_states"."active_anomaly_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL));--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_owner_states_unreviewed_org" ON "cost_insight_owner_states" USING btree ("owned_by_organization_id","updated_at") WHERE "cost_insight_owner_states"."owned_by_organization_id" IS NOT NULL AND (("cost_insight_owner_states"."active_anomaly_event_id" IS NOT NULL AND "cost_insight_owner_states"."active_anomaly_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL));--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_degraded_intervals_unresolved" ON "cost_insight_rollup_degraded_intervals" USING btree ("start_hour","end_hour_exclusive") WHERE "cost_insight_rollup_degraded_intervals"."resolved_at" is null; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0174_snapshot.json b/packages/db/src/migrations/meta/0174_snapshot.json new file mode 100644 index 0000000000..72e267f4c9 --- /dev/null +++ b/packages/db/src/migrations/meta/0174_snapshot.json @@ -0,0 +1,34109 @@ +{ + "id": "e22596a2-8774-48f3-a63f-792566ac90c0", + "prevId": "06265fd6-428e-4b7d-9092-dba307d6255e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_configs": { + "name": "agent_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "runtime_state": { + "name": "runtime_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_configs_org_id": { + "name": "IDX_agent_configs_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_owned_by_user_id": { + "name": "IDX_agent_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_agent_type": { + "name": "IDX_agent_configs_agent_type", + "columns": [ + { + "expression": "agent_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_platform": { + "name": "IDX_agent_configs_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_configs_owned_by_organization_id_organizations_id_fk": { + "name": "agent_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_configs_org_agent_platform": { + "name": "UQ_agent_configs_org_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "agent_type", + "platform" + ] + }, + "UQ_agent_configs_user_agent_platform": { + "name": "UQ_agent_configs_user_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_user_id", + "agent_type", + "platform" + ] + } + }, + "policies": {}, + "checkConstraints": { + "agent_configs_owner_check": { + "name": "agent_configs_owner_check", + "value": "(\n (\"agent_configs\".\"owned_by_user_id\" IS NOT NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_configs\".\"owned_by_user_id\" IS NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "agent_configs_agent_type_check": { + "name": "agent_configs_agent_type_check", + "value": "\"agent_configs\".\"agent_type\" IN ('code_review', 'auto_triage', 'auto_fix', 'security_scan')" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_agents": { + "name": "agent_environment_profile_agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_agents_profile_id": { + "name": "IDX_agent_env_profile_agents_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_agents", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_agents_profile_slug": { + "name": "UQ_agent_env_profile_agents_profile_slug", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_commands": { + "name": "agent_environment_profile_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_commands_profile_id": { + "name": "IDX_agent_env_profile_commands_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_commands_profile_sequence": { + "name": "UQ_agent_env_profile_commands_profile_sequence", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_kilo_commands": { + "name": "agent_environment_profile_kilo_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtask": { + "name": "subtask", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_kilo_cmds_profile_id": { + "name": "IDX_agent_env_profile_kilo_cmds_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_kilo_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_kilo_cmds_profile_name": { + "name": "UQ_agent_env_profile_kilo_cmds_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_mcp_servers": { + "name": "agent_environment_profile_mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_mcp_servers_profile_id": { + "name": "IDX_agent_env_profile_mcp_servers_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_mcp_servers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_mcp_servers_profile_name": { + "name": "UQ_agent_env_profile_mcp_servers_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_repo_bindings": { + "name": "agent_environment_profile_repo_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profile_repo_bindings_user": { + "name": "UQ_agent_env_profile_repo_bindings_user", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profile_repo_bindings_org": { + "name": "UQ_agent_env_profile_repo_bindings_org", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profile_repo_bindings_owner_check": { + "name": "agent_env_profile_repo_bindings_owner_check", + "value": "(\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_skills": { + "name": "agent_environment_profile_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_markdown": { + "name": "raw_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_skills_profile_id": { + "name": "IDX_agent_env_profile_skills_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_skills", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_skills_profile_name": { + "name": "UQ_agent_env_profile_skills_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_vars": { + "name": "agent_environment_profile_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_vars_profile_id": { + "name": "IDX_agent_env_profile_vars_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_vars", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_vars_profile_key": { + "name": "UQ_agent_env_profile_vars_profile_key", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profiles": { + "name": "agent_environment_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profiles_org_name": { + "name": "UQ_agent_env_profiles_org_name", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_name": { + "name": "UQ_agent_env_profiles_user_name", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_org_default": { + "name": "UQ_agent_env_profiles_org_default", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_default": { + "name": "UQ_agent_env_profiles_user_default", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_org_id": { + "name": "IDX_agent_env_profiles_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_user_id": { + "name": "IDX_agent_env_profiles_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_created_by_user_id": { + "name": "IDX_agent_env_profiles_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profiles_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profiles_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profiles_owner_check": { + "name": "agent_env_profiles_owner_check", + "value": "(\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.api_kind": { + "name": "api_kind", + "schema": "", + "columns": { + "api_kind_id": { + "name": "api_kind_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_api_kind": { + "name": "UQ_api_kind", + "columns": [ + { + "expression": "api_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_compress_log": { + "name": "api_request_compress_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_api_request_compress_log_created_at": { + "name": "idx_api_request_compress_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_log": { + "name": "api_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_api_request_log_created_at": { + "name": "idx_api_request_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_feedback": { + "name": "app_builder_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_status": { + "name": "preview_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_feedback_created_at": { + "name": "IDX_app_builder_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_kilo_user_id": { + "name": "IDX_app_builder_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_project_id": { + "name": "IDX_app_builder_feedback_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "app_builder_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "app_builder_feedback_project_id_app_builder_projects_id_fk": { + "name": "app_builder_feedback_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_project_sessions": { + "name": "app_builder_project_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "worker_version": { + "name": "worker_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'v2'" + } + }, + "indexes": { + "IDX_app_builder_project_sessions_project_id": { + "name": "IDX_app_builder_project_sessions_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_project_sessions_project_id_app_builder_projects_id_fk": { + "name": "app_builder_project_sessions_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_project_sessions", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_app_builder_project_sessions_cloud_agent_session_id": { + "name": "UQ_app_builder_project_sessions_cloud_agent_session_id", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_projects": { + "name": "app_builder_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "git_repo_full_name": { + "name": "git_repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_platform_integration_id": { + "name": "git_platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_projects_created_by_user_id": { + "name": "IDX_app_builder_projects_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_user_id": { + "name": "IDX_app_builder_projects_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_organization_id": { + "name": "IDX_app_builder_projects_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_created_at": { + "name": "IDX_app_builder_projects_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_last_message_at": { + "name": "IDX_app_builder_projects_last_message_at", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_git_repo_integration": { + "name": "IDX_app_builder_projects_git_repo_integration", + "columns": [ + { + "expression": "git_repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"app_builder_projects\".\"git_repo_full_name\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_projects_owned_by_user_id_kilocode_users_id_fk": { + "name": "app_builder_projects_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_owned_by_organization_id_organizations_id_fk": { + "name": "app_builder_projects_owned_by_organization_id_organizations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_deployment_id_deployments_id_fk": { + "name": "app_builder_projects_deployment_id_deployments_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk": { + "name": "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "platform_integrations", + "columnsFrom": [ + "git_platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "app_builder_projects_owner_check": { + "name": "app_builder_projects_owner_check", + "value": "(\n (\"app_builder_projects\".\"owned_by_user_id\" IS NOT NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NULL) OR\n (\"app_builder_projects\".\"owned_by_user_id\" IS NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.app_min_versions": { + "name": "app_min_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ios_min_version": { + "name": "ios_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "android_min_version": { + "name": "android_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_reported_messages": { + "name": "app_reported_messages", + "schema": "", + "columns": { + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "app_reported_messages_cli_session_id_cli_sessions_session_id_fk": { + "name": "app_reported_messages_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "app_reported_messages", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_fix_tickets": { + "name": "auto_fix_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "triage_ticket_id": { + "name": "triage_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'label'" + }, + "review_comment_id": { + "name": "review_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_comment_body": { + "name": "review_comment_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "diff_hunk": { + "name": "diff_hunk", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_ref": { + "name": "pr_head_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_branch": { + "name": "pr_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_fix_tickets_repo_issue": { + "name": "UQ_auto_fix_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"trigger_source\" = 'label'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_fix_tickets_repo_review_comment": { + "name": "UQ_auto_fix_tickets_repo_review_comment", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "review_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"review_comment_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_org": { + "name": "IDX_auto_fix_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_user": { + "name": "IDX_auto_fix_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_status": { + "name": "IDX_auto_fix_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_created_at": { + "name": "IDX_auto_fix_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_triage_ticket_id": { + "name": "IDX_auto_fix_tickets_triage_ticket_id", + "columns": [ + { + "expression": "triage_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_session_id": { + "name": "IDX_auto_fix_tickets_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_fix_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_fix_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "triage_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk": { + "name": "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_fix_tickets_owner_check": { + "name": "auto_fix_tickets_owner_check", + "value": "(\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_fix_tickets_status_check": { + "name": "auto_fix_tickets_status_check", + "value": "\"auto_fix_tickets\".\"status\" IN ('pending', 'running', 'completed', 'failed', 'cancelled')" + }, + "auto_fix_tickets_classification_check": { + "name": "auto_fix_tickets_classification_check", + "value": "\"auto_fix_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'unclear')" + }, + "auto_fix_tickets_confidence_check": { + "name": "auto_fix_tickets_confidence_check", + "value": "\"auto_fix_tickets\".\"confidence\" >= 0 AND \"auto_fix_tickets\".\"confidence\" <= 1" + }, + "auto_fix_tickets_trigger_source_check": { + "name": "auto_fix_tickets_trigger_source_check", + "value": "\"auto_fix_tickets\".\"trigger_source\" IN ('label', 'review_comment')" + } + }, + "isRLSEnabled": false + }, + "public.auto_model": { + "name": "auto_model", + "schema": "", + "columns": { + "auto_model_id": { + "name": "auto_model_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_auto_model": { + "name": "UQ_auto_model", + "columns": [ + { + "expression": "auto_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_top_up_configs": { + "name": "auto_top_up_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_method_id": { + "name": "stripe_payment_method_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "last_auto_top_up_at": { + "name": "last_auto_top_up_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_started_at": { + "name": "attempt_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_top_up_configs_owned_by_user_id": { + "name": "UQ_auto_top_up_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_top_up_configs_owned_by_organization_id": { + "name": "UQ_auto_top_up_configs_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "auto_top_up_configs_owned_by_organization_id_organizations_id_fk": { + "name": "auto_top_up_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_top_up_configs_exactly_one_owner": { + "name": "auto_top_up_configs_exactly_one_owner", + "value": "(\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NULL) OR (\"auto_top_up_configs\".\"owned_by_user_id\" IS NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.auto_triage_tickets": { + "name": "auto_triage_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_duplicate": { + "name": "is_duplicate", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duplicate_of_ticket_id": { + "name": "duplicate_of_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "similarity_score": { + "name": "similarity_score", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "qdrant_point_id": { + "name": "qdrant_point_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "should_auto_fix": { + "name": "should_auto_fix", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "action_taken": { + "name": "action_taken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_metadata": { + "name": "action_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_triage_tickets_repo_issue": { + "name": "UQ_auto_triage_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_org": { + "name": "IDX_auto_triage_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_user": { + "name": "IDX_auto_triage_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_status": { + "name": "IDX_auto_triage_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_created_at": { + "name": "IDX_auto_triage_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_qdrant_point_id": { + "name": "IDX_auto_triage_tickets_qdrant_point_id", + "columns": [ + { + "expression": "qdrant_point_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owner_status_created": { + "name": "IDX_auto_triage_tickets_owner_status_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_user_status_created": { + "name": "IDX_auto_triage_tickets_user_status_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_repo_classification": { + "name": "IDX_auto_triage_tickets_repo_classification", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "classification", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_triage_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_triage_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "duplicate_of_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_triage_tickets_owner_check": { + "name": "auto_triage_tickets_owner_check", + "value": "(\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_triage_tickets_issue_type_check": { + "name": "auto_triage_tickets_issue_type_check", + "value": "\"auto_triage_tickets\".\"issue_type\" IN ('issue', 'pull_request')" + }, + "auto_triage_tickets_classification_check": { + "name": "auto_triage_tickets_classification_check", + "value": "\"auto_triage_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'duplicate', 'unclear')" + }, + "auto_triage_tickets_confidence_check": { + "name": "auto_triage_tickets_confidence_check", + "value": "\"auto_triage_tickets\".\"confidence\" >= 0 AND \"auto_triage_tickets\".\"confidence\" <= 1" + }, + "auto_triage_tickets_similarity_score_check": { + "name": "auto_triage_tickets_similarity_score_check", + "value": "\"auto_triage_tickets\".\"similarity_score\" >= 0 AND \"auto_triage_tickets\".\"similarity_score\" <= 1" + }, + "auto_triage_tickets_status_check": { + "name": "auto_triage_tickets_status_check", + "value": "\"auto_triage_tickets\".\"status\" IN ('pending', 'analyzing', 'actioned', 'failed', 'skipped')" + }, + "auto_triage_tickets_action_taken_check": { + "name": "auto_triage_tickets_action_taken_check", + "value": "\"auto_triage_tickets\".\"action_taken\" IN ('pr_created', 'comment_posted', 'closed_duplicate', 'needs_clarification')" + } + }, + "isRLSEnabled": false + }, + "public.bot_request_cloud_agent_sessions": { + "name": "bot_request_cloud_agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "bot_request_id": { + "name": "bot_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spawn_group_id": { + "name": "spawn_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlab_project": { + "name": "gitlab_project", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_step": { + "name": "callback_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message": { + "name": "final_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message_fetched_at": { + "name": "final_message_fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "final_message_error": { + "name": "final_message_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "continuation_started_at": { + "name": "continuation_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_bot_request_cas_cloud_agent_session_id": { + "name": "UQ_bot_request_cas_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id": { + "name": "IDX_bot_request_cas_bot_request_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id_status": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id_status", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk": { + "name": "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk", + "tableFrom": "bot_request_cloud_agent_sessions", + "tableTo": "bot_requests", + "columnsFrom": [ + "bot_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_requests": { + "name": "bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_thread_id": { + "name": "platform_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_message_id": { + "name": "platform_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_bot_requests_created_at": { + "name": "IDX_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_created_by": { + "name": "IDX_bot_requests_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_organization_id": { + "name": "IDX_bot_requests_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_platform_integration_id": { + "name": "IDX_bot_requests_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_status": { + "name": "IDX_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_requests_created_by_kilocode_users_id_fk": { + "name": "bot_requests_created_by_kilocode_users_id_fk", + "tableFrom": "bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_organization_id_organizations_id_fk": { + "name": "bot_requests_organization_id_organizations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.byok_api_keys": { + "name": "byok_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "management_source": { + "name": "management_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_byok_api_keys_organization_id": { + "name": "IDX_byok_api_keys_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_kilo_user_id": { + "name": "IDX_byok_api_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_provider_id": { + "name": "IDX_byok_api_keys_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "byok_api_keys_organization_id_organizations_id_fk": { + "name": "byok_api_keys_organization_id_organizations_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "byok_api_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "byok_api_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_byok_api_keys_org_provider": { + "name": "UQ_byok_api_keys_org_provider", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "provider_id" + ] + }, + "UQ_byok_api_keys_user_provider": { + "name": "UQ_byok_api_keys_user_provider", + "nullsNotDistinct": false, + "columns": [ + "kilo_user_id", + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "byok_api_keys_management_source_check": { + "name": "byok_api_keys_management_source_check", + "value": "\"byok_api_keys\".\"management_source\" IN ('user', 'coding_plan')" + }, + "byok_api_keys_owner_check": { + "name": "byok_api_keys_owner_check", + "value": "(\n (\"byok_api_keys\".\"kilo_user_id\" IS NOT NULL AND \"byok_api_keys\".\"organization_id\" IS NULL) OR\n (\"byok_api_keys\".\"kilo_user_id\" IS NULL AND \"byok_api_keys\".\"organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cli_sessions": { + "name": "cli_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_mode": { + "name": "last_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_kilo_user_id": { + "name": "IDX_cli_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_created_at": { + "name": "IDX_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_updated_at": { + "name": "IDX_cli_sessions_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_organization_id": { + "name": "IDX_cli_sessions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_user_updated": { + "name": "IDX_cli_sessions_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_forked_from_cli_sessions_session_id_fk": { + "name": "cli_sessions_forked_from_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "forked_from" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_parent_session_id_cli_sessions_session_id_fk": { + "name": "cli_sessions_parent_session_id_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "parent_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_organization_id_organizations_id_fk": { + "name": "cli_sessions_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cli_sessions_cloud_agent_session_id_unique": { + "name": "cli_sessions_cloud_agent_session_id_unique", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_sessions_v2": { + "name": "cli_sessions_v2", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_updated_at": { + "name": "status_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_v2_parent_session_id_kilo_user_id": { + "name": "IDX_cli_sessions_v2_parent_session_id_kilo_user_id", + "columns": [ + { + "expression": "parent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_public_id": { + "name": "UQ_cli_sessions_v2_public_id", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"public_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_cloud_agent_session_id": { + "name": "UQ_cli_sessions_v2_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"cloud_agent_session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_organization_id": { + "name": "IDX_cli_sessions_v2_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_kilo_user_id": { + "name": "IDX_cli_sessions_v2_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_created_at": { + "name": "IDX_cli_sessions_v2_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_updated": { + "name": "IDX_cli_sessions_v2_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_sessions_v2_git_url_branch_idx": { + "name": "cli_sessions_v2_git_url_branch_idx", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_v2_organization_id_organizations_id_fk": { + "name": "cli_sessions_v2_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_v2_parent_session_id_kilo_user_id_fk": { + "name": "cli_sessions_v2_parent_session_id_kilo_user_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "cli_sessions_v2", + "columnsFrom": [ + "parent_session_id", + "kilo_user_id" + ], + "columnsTo": [ + "session_id", + "kilo_user_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cli_sessions_v2_session_id_kilo_user_id_pk": { + "name": "cli_sessions_v2_session_id_kilo_user_id_pk", + "columns": [ + "session_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_code_review_attempts": { + "name": "cloud_agent_code_review_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retry_reason": { + "name": "retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analytics_enabled_at_dispatch": { + "name": "analytics_enabled_at_dispatch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_review_attempts_review_attempt_number": { + "name": "UQ_cloud_agent_code_review_attempts_review_attempt_number", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_code_review_id": { + "name": "idx_cloud_agent_code_review_attempts_code_review_id", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_session_id": { + "name": "idx_cloud_agent_code_review_attempts_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_cli_session_id": { + "name": "idx_cloud_agent_code_review_attempts_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_status": { + "name": "idx_cloud_agent_code_review_attempts_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_retry_reason": { + "name": "idx_cloud_agent_code_review_attempts_retry_reason", + "columns": [ + { + "expression": "retry_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "retry_of_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_review_attempts_attempt_number_check": { + "name": "cloud_agent_code_review_attempts_attempt_number_check", + "value": "\"cloud_agent_code_review_attempts\".\"attempt_number\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_code_reviews": { + "name": "cloud_agent_code_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author": { + "name": "pr_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author_github_id": { + "name": "pr_author_github_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_ref": { + "name": "head_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "dispatch_reservation_id": { + "name": "dispatch_reservation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'v1'" + }, + "check_run_id": { + "name": "check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_used": { + "name": "repository_review_instructions_used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "repository_review_instructions_ref": { + "name": "repository_review_instructions_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_truncated": { + "name": "repository_review_instructions_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previous_summary_body": { + "name": "previous_summary_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_summary_head_sha": { + "name": "previous_summary_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_tokens_in": { + "name": "total_tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens_out": { + "name": "total_tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cost_musd": { + "name": "total_cost_musd", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_reviews_repo_pr_sha": { + "name": "UQ_cloud_agent_code_reviews_repo_pr_sha", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_org_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_user_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_session_id": { + "name": "idx_cloud_agent_code_reviews_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_cli_session_id": { + "name": "idx_cloud_agent_code_reviews_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_status": { + "name": "idx_cloud_agent_code_reviews_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_repo": { + "name": "idx_cloud_agent_code_reviews_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_number": { + "name": "idx_cloud_agent_code_reviews_pr_number", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_created_at": { + "name": "idx_cloud_agent_code_reviews_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_author_github_id": { + "name": "idx_cloud_agent_code_reviews_pr_author_github_id", + "columns": [ + { + "expression": "pr_author_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk": { + "name": "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_reviews_owner_check": { + "name": "cloud_agent_code_reviews_owner_check", + "value": "(\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NULL) OR\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_feedback": { + "name": "cloud_agent_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cloud_agent_feedback_created_at": { + "name": "IDX_cloud_agent_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_kilo_user_id": { + "name": "IDX_cloud_agent_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_cloud_agent_session_id": { + "name": "IDX_cloud_agent_feedback_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "cloud_agent_feedback_organization_id_organizations_id_fk": { + "name": "cloud_agent_feedback_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_session_runs": { + "name": "cloud_agent_session_runs", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapper_run_id": { + "name": "wrapper_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dispatch_accepted_at": { + "name": "dispatch_accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_activity_observed_at": { + "name": "agent_activity_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_cloud_agent_session_runs_wrapper_run_id": { + "name": "IDX_cloud_agent_session_runs_wrapper_run_id", + "columns": [ + { + "expression": "wrapper_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"wrapper_run_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_session_queued": { + "name": "IDX_cloud_agent_session_runs_session_queued", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_queued_at": { + "name": "IDX_cloud_agent_session_runs_queued_at", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_terminal_at": { + "name": "IDX_cloud_agent_session_runs_terminal_at", + "columns": [ + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_status_terminal": { + "name": "IDX_cloud_agent_session_runs_status_terminal", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_failure_terminal": { + "name": "IDX_cloud_agent_session_runs_failure_terminal", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_error_expires_at": { + "name": "IDX_cloud_agent_session_runs_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk", + "tableFrom": "cloud_agent_session_runs", + "tableTo": "cloud_agent_sessions", + "columnsFrom": [ + "cloud_agent_session_id" + ], + "columnsTo": [ + "cloud_agent_session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk", + "columns": [ + "cloud_agent_session_id", + "message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_session_runs_status_check": { + "name": "cloud_agent_session_runs_status_check", + "value": "\"cloud_agent_session_runs\".\"status\" IN ('queued', 'accepted', 'completed', 'failed', 'interrupted')" + }, + "cloud_agent_session_runs_failure_classification_check": { + "name": "cloud_agent_session_runs_failure_classification_check", + "value": "(\"cloud_agent_session_runs\".\"failure_stage\" IS NULL AND \"cloud_agent_session_runs\".\"failure_code\" IS NULL) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'pre_dispatch' AND \"cloud_agent_session_runs\".\"failure_code\" IN ('sandbox_connect_failed', 'workspace_setup_failed', 'kilo_server_failed', 'wrapper_start_failed', 'invalid_delivery_request', 'session_metadata_missing', 'model_missing', 'delivery_failure_unknown')) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'post_dispatch_no_activity' AND \"cloud_agent_session_runs\".\"failure_code\" IN ('wrapper_disconnected', 'wrapper_no_output', 'wrapper_ping_timeout', 'wrapper_error_before_activity', 'missing_assistant_reply')) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'agent_activity' AND \"cloud_agent_session_runs\".\"failure_code\" IN ('assistant_error', 'wrapper_error_after_activity')) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'interruption' AND \"cloud_agent_session_runs\".\"failure_code\" IN ('user_interrupt', 'container_shutdown', 'system_interrupt')) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'unknown' AND \"cloud_agent_session_runs\".\"failure_code\" = 'unclassified')" + }, + "cloud_agent_session_runs_error_message_bounded_check": { + "name": "cloud_agent_session_runs_error_message_bounded_check", + "value": "\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_session_runs\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_session_runs_error_expiry_check": { + "name": "cloud_agent_session_runs_error_expiry_check", + "value": "(\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_session_runs\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_sessions": { + "name": "cloud_agent_sessions", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initial_message_id": { + "name": "initial_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "failure_at": { + "name": "failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_cloud_agent_sessions_kilo_session_id": { + "name": "UQ_cloud_agent_sessions_kilo_session_id", + "columns": [ + { + "expression": "kilo_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_sessions_initial_message_id": { + "name": "UQ_cloud_agent_sessions_initial_message_id", + "columns": [ + { + "expression": "initial_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_sandbox_id": { + "name": "IDX_cloud_agent_sessions_sandbox_id", + "columns": [ + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"sandbox_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_created_at": { + "name": "IDX_cloud_agent_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_created": { + "name": "IDX_cloud_agent_sessions_failure_created", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_at": { + "name": "IDX_cloud_agent_sessions_failure_at", + "columns": [ + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_classification_at": { + "name": "IDX_cloud_agent_sessions_failure_classification_at", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_error_expires_at": { + "name": "IDX_cloud_agent_sessions_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_sessions_failure_classification_check": { + "name": "cloud_agent_sessions_failure_classification_check", + "value": "(\"cloud_agent_sessions\".\"failure_at\" IS NULL AND \"cloud_agent_sessions\".\"failure_stage\" IS NULL AND \"cloud_agent_sessions\".\"failure_code\" IS NULL) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'sandbox_identity' AND \"cloud_agent_sessions\".\"failure_code\" = 'sandbox_id_derivation_failed') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'registration' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_registration_rejected') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'initial_admission' AND \"cloud_agent_sessions\".\"failure_code\" IN ('initial_admission_rejected', 'initial_queue_full', 'invalid_initial_intent')) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'transport' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_rpc_outcome_unknown')" + }, + "cloud_agent_sessions_error_message_bounded_check": { + "name": "cloud_agent_sessions_error_message_bounded_check", + "value": "\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_sessions\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_sessions_error_expiry_check": { + "name": "cloud_agent_sessions_error_expiry_check", + "value": "(\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_sessions\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_webhook_triggers": { + "name": "cloud_agent_webhook_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cloud_agent'" + }, + "kiloclaw_instance_id": { + "name": "kiloclaw_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "activation_mode": { + "name": "activation_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'webhook'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_timezone": { + "name": "cron_timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'UTC'" + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_webhook_triggers_user_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_user_trigger", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_webhook_triggers_org_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_org_trigger", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_user": { + "name": "IDX_cloud_agent_webhook_triggers_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_org": { + "name": "IDX_cloud_agent_webhook_triggers_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_active": { + "name": "IDX_cloud_agent_webhook_triggers_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_profile": { + "name": "IDX_cloud_agent_webhook_triggers_profile", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_organization_id_organizations_id_fk": { + "name": "cloud_agent_webhook_triggers_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk": { + "name": "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "kiloclaw_instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk": { + "name": "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_cloud_agent_webhook_triggers_owner": { + "name": "CHK_cloud_agent_webhook_triggers_owner", + "value": "(\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NULL) OR\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_cloud_agent_fields": { + "name": "CHK_cloud_agent_webhook_triggers_cloud_agent_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'cloud_agent' OR\n (\"cloud_agent_webhook_triggers\".\"github_repo\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"profile_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_kiloclaw_fields": { + "name": "CHK_cloud_agent_webhook_triggers_kiloclaw_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'kiloclaw_chat' OR\n \"cloud_agent_webhook_triggers\".\"kiloclaw_instance_id\" IS NOT NULL\n )" + }, + "CHK_cloud_agent_webhook_triggers_scheduled_fields": { + "name": "CHK_cloud_agent_webhook_triggers_scheduled_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"activation_mode\" != 'scheduled' OR\n \"cloud_agent_webhook_triggers\".\"cron_expression\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_indexing_manifest": { + "name": "code_indexing_manifest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_lines": { + "name": "total_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_ai_lines": { + "name": "total_ai_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_manifest_organization_id": { + "name": "IDX_code_indexing_manifest_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_kilo_user_id": { + "name": "IDX_code_indexing_manifest_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_project_id": { + "name": "IDX_code_indexing_manifest_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_git_branch": { + "name": "IDX_code_indexing_manifest_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_created_at": { + "name": "IDX_code_indexing_manifest_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_manifest", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_indexing_manifest_org_user_project_hash_branch": { + "name": "UQ_code_indexing_manifest_org_user_project_hash_branch", + "nullsNotDistinct": true, + "columns": [ + "organization_id", + "kilo_user_id", + "project_id", + "file_path", + "git_branch" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_indexing_search": { + "name": "code_indexing_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_search_organization_id": { + "name": "IDX_code_indexing_search_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_kilo_user_id": { + "name": "IDX_code_indexing_search_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_project_id": { + "name": "IDX_code_indexing_search_project_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_created_at": { + "name": "IDX_code_indexing_search_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_search_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_search_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_search", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_review_analytics_findings": { + "name": "code_review_analytics_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "analytics_result_id": { + "name": "analytics_result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "security_class": { + "name": "security_class", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk": { + "name": "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk", + "tableFrom": "code_review_analytics_findings", + "tableTo": "code_review_analytics_results", + "columnsFrom": [ + "analytics_result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_findings_result_ordinal": { + "name": "UQ_code_review_analytics_findings_result_ordinal", + "nullsNotDistinct": false, + "columns": [ + "analytics_result_id", + "ordinal" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_findings_severity_check": { + "name": "code_review_analytics_findings_severity_check", + "value": "\"code_review_analytics_findings\".\"severity\" IN ('critical', 'warning', 'suggestion')" + }, + "code_review_analytics_findings_category_check": { + "name": "code_review_analytics_findings_category_check", + "value": "\"code_review_analytics_findings\".\"category\" IN ('security', 'correctness', 'reliability', 'data_integrity', 'performance', 'compatibility', 'maintainability', 'test_quality', 'documentation', 'accessibility', 'other')" + }, + "code_review_analytics_findings_security_class_check": { + "name": "code_review_analytics_findings_security_class_check", + "value": "\"code_review_analytics_findings\".\"security_class\" IN ('auth_access', 'injection', 'data_protection', 'request_resource_boundary', 'deserialization_object_integrity', 'dependency_supply_chain', 'memory_safety', 'availability', 'concurrency', 'security_configuration', 'other')" + }, + "code_review_analytics_findings_ordinal_check": { + "name": "code_review_analytics_findings_ordinal_check", + "value": "\"code_review_analytics_findings\".\"ordinal\" >= 0" + }, + "code_review_analytics_findings_security_class_presence_check": { + "name": "code_review_analytics_findings_security_class_presence_check", + "value": "(\n (\"code_review_analytics_findings\".\"category\" = 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NOT NULL) OR\n (\"code_review_analytics_findings\".\"category\" <> 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_analytics_results": { + "name": "code_review_analytics_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_attempt_id": { + "name": "source_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "capture_status": { + "name": "capture_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "taxonomy_version": { + "name": "taxonomy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_level": { + "name": "impact_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "complexity_level": { + "name": "complexity_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification_confidence": { + "name": "classification_confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finalized_at": { + "name": "finalized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_analytics_results_source_attempt_id": { + "name": "idx_code_review_analytics_results_source_attempt_id", + "columns": [ + { + "expression": "source_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_analytics_results_finalized_at": { + "name": "idx_code_review_analytics_results_finalized_at", + "columns": [ + { + "expression": "finalized_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "source_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_results_code_review_id": { + "name": "UQ_code_review_analytics_results_code_review_id", + "nullsNotDistinct": false, + "columns": [ + "code_review_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_results_capture_status_check": { + "name": "code_review_analytics_results_capture_status_check", + "value": "\"code_review_analytics_results\".\"capture_status\" IN ('captured', 'missing', 'invalid', 'omitted')" + }, + "code_review_analytics_results_change_type_check": { + "name": "code_review_analytics_results_change_type_check", + "value": "\"code_review_analytics_results\".\"change_type\" IN ('bug_fix', 'feature', 'refactor', 'maintenance', 'dependency', 'test', 'documentation', 'mixed', 'other')" + }, + "code_review_analytics_results_impact_level_check": { + "name": "code_review_analytics_results_impact_level_check", + "value": "\"code_review_analytics_results\".\"impact_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_complexity_level_check": { + "name": "code_review_analytics_results_complexity_level_check", + "value": "\"code_review_analytics_results\".\"complexity_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_confidence_check": { + "name": "code_review_analytics_results_classification_confidence_check", + "value": "\"code_review_analytics_results\".\"classification_confidence\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_presence_check": { + "name": "code_review_analytics_results_classification_presence_check", + "value": "(\n (\n \"code_review_analytics_results\".\"capture_status\" = 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NOT NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NOT NULL\n ) OR (\n \"code_review_analytics_results\".\"capture_status\" <> 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NULL\n )\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_feedback_events": { + "name": "code_review_feedback_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "kilo_comment_id": { + "name": "kilo_comment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_excerpt": { + "name": "reply_excerpt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_comment_excerpt": { + "name": "kilo_comment_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedupe_hash": { + "name": "dedupe_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_feedback_events_owned_by_org_id": { + "name": "idx_code_review_feedback_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_owned_by_user_id": { + "name": "idx_code_review_feedback_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_platform_repo": { + "name": "idx_code_review_feedback_events_platform_repo", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_created_at": { + "name": "idx_code_review_feedback_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_feedback_events_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_feedback_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_feedback_events_dedupe_hash": { + "name": "UQ_code_review_feedback_events_dedupe_hash", + "nullsNotDistinct": false, + "columns": [ + "dedupe_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_feedback_events_owner_check": { + "name": "code_review_feedback_events_owner_check", + "value": "(\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_memory_proposals": { + "name": "code_review_memory_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_markdown": { + "name": "proposed_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "positive_count": { + "name": "positive_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "negative_count": { + "name": "negative_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "neutral_count": { + "name": "neutral_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "change_request_url": { + "name": "change_request_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_memory_proposals_owned_by_org_id": { + "name": "idx_code_review_memory_proposals_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_owned_by_user_id": { + "name": "idx_code_review_memory_proposals_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_platform_repo_status": { + "name": "idx_code_review_memory_proposals_platform_repo_status", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_updated_at": { + "name": "idx_code_review_memory_proposals_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_org_active_scope": { + "name": "UQ_code_review_memory_proposals_org_active_scope", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_user_active_scope": { + "name": "UQ_code_review_memory_proposals_user_active_scope", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "code_review_memory_proposals_owner_check": { + "name": "code_review_memory_proposals_owner_check", + "value": "(\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_availability_intents": { + "name": "coding_plan_availability_intents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_availability_intents_user_plan": { + "name": "UQ_coding_plan_availability_intents_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_availability_intents_plan": { + "name": "IDX_coding_plan_availability_intents_plan", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_availability_intents_user_id_kilocode_users_id_fk": { + "name": "coding_plan_availability_intents_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_availability_intents", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coding_plan_key_inventory": { + "name": "coding_plan_key_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_plan_id": { + "name": "upstream_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_fingerprint": { + "name": "credential_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "assigned_to_user_id": { + "name": "assigned_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_requested_at": { + "name": "revocation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_attempt_count": { + "name": "revocation_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_revocation_error": { + "name": "last_revocation_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_key_inv_fingerprint": { + "name": "UQ_coding_plan_key_inv_fingerprint", + "columns": [ + { + "expression": "credential_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_plan_status": { + "name": "IDX_coding_plan_key_inv_plan_status", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_available": { + "name": "IDX_coding_plan_key_inv_available", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"coding_plan_key_inventory\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk": { + "name": "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_key_inventory", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_to_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_key_inventory_status_check": { + "name": "coding_plan_key_inventory_status_check", + "value": "\"coding_plan_key_inventory\".\"status\" IN ('available', 'assigned', 'revocation_pending', 'revoked', 'revocation_failed')" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_subscriptions": { + "name": "coding_plan_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_inventory_id": { + "name": "key_inventory_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "installed_byok_key_id": { + "name": "installed_byok_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "billing_period_days": { + "name": "billing_period_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "past_due_started_at": { + "name": "past_due_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_grace_expires_at": { + "name": "payment_grace_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_attempted_for_due": { + "name": "auto_top_up_attempted_for_due", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_sub_live_user_plan": { + "name": "UQ_coding_plan_sub_live_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_status": { + "name": "IDX_coding_plan_sub_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_renewal": { + "name": "IDX_coding_plan_sub_renewal", + "columns": [ + { + "expression": "credit_renewal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_inventory": { + "name": "IDX_coding_plan_sub_inventory", + "columns": [ + { + "expression": "key_inventory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_subscriptions_user_id_kilocode_users_id_fk": { + "name": "coding_plan_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk": { + "name": "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "coding_plan_key_inventory", + "columnsFrom": [ + "key_inventory_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk": { + "name": "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "byok_api_keys", + "columnsFrom": [ + "installed_byok_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_subscriptions_status_check": { + "name": "coding_plan_subscriptions_status_check", + "value": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due', 'canceled')" + }, + "coding_plan_subscriptions_live_access_check": { + "name": "coding_plan_subscriptions_live_access_check", + "value": "\"coding_plan_subscriptions\".\"status\" = 'canceled' OR \"coding_plan_subscriptions\".\"key_inventory_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_terms": { + "name": "coding_plan_terms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_terms_request": { + "name": "UQ_coding_plan_terms_request", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_terms_subscription": { + "name": "IDX_coding_plan_terms_subscription", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk": { + "name": "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "coding_plan_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_user_id_kilocode_users_id_fk": { + "name": "coding_plan_terms_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk": { + "name": "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_terms_kind_check": { + "name": "coding_plan_terms_kind_check", + "value": "\"coding_plan_terms\".\"kind\" IN ('activation', 'extension', 'renewal')" + } + }, + "isRLSEnabled": false + }, + "public.contributor_champion_contributors": { + "name": "contributor_champion_contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_profile_url": { + "name": "github_profile_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "first_contribution_at": { + "name": "first_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_contribution_at": { + "name": "last_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "all_time_contributions": { + "name": "all_time_contributions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "manual_email": { + "name": "manual_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_contributors_last_contribution_at": { + "name": "IDX_contributor_champion_contributors_last_contribution_at", + "columns": [ + { + "expression": "last_contribution_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_contributors_manual_email": { + "name": "IDX_contributor_champion_contributors_manual_email", + "columns": [ + { + "expression": "manual_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_contributors_github_login": { + "name": "UQ_contributor_champion_contributors_github_login", + "nullsNotDistinct": false, + "columns": [ + "github_login" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_events": { + "name": "contributor_champion_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_number": { + "name": "github_pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "github_pr_url": { + "name": "github_pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_title": { + "name": "github_pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_login": { + "name": "github_author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_email": { + "name": "github_author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_events_contributor_id": { + "name": "IDX_contributor_champion_events_contributor_id", + "columns": [ + { + "expression": "contributor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_merged_at": { + "name": "IDX_contributor_champion_events_merged_at", + "columns": [ + { + "expression": "merged_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_author_email": { + "name": "IDX_contributor_champion_events_author_email", + "columns": [ + { + "expression": "github_author_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_events", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_events_repo_pr": { + "name": "UQ_contributor_champion_events_repo_pr", + "nullsNotDistinct": false, + "columns": [ + "repo_full_name", + "github_pr_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_memberships": { + "name": "contributor_champion_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selected_tier": { + "name": "selected_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_tier": { + "name": "enrolled_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_amount_microdollars": { + "name": "credit_amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credits_last_granted_at": { + "name": "credits_last_granted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "linked_kilo_user_id": { + "name": "linked_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_memberships_credits_due": { + "name": "IDX_contributor_champion_memberships_credits_due", + "columns": [ + { + "expression": "credits_last_granted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NOT NULL AND \"contributor_champion_memberships\".\"credit_amount_microdollars\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_memberships_linked_kilo_user_id": { + "name": "IDX_contributor_champion_memberships_linked_kilo_user_id", + "columns": [ + { + "expression": "linked_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk": { + "name": "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "kilocode_users", + "columnsFrom": [ + "linked_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_memberships_contributor_id": { + "name": "UQ_contributor_champion_memberships_contributor_id", + "nullsNotDistinct": false, + "columns": [ + "contributor_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contributor_champion_memberships_selected_tier_check": { + "name": "contributor_champion_memberships_selected_tier_check", + "value": "\"contributor_champion_memberships\".\"selected_tier\" IS NULL OR \"contributor_champion_memberships\".\"selected_tier\" IN ('contributor', 'ambassador', 'champion')" + }, + "contributor_champion_memberships_enrolled_tier_check": { + "name": "contributor_champion_memberships_enrolled_tier_check", + "value": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NULL OR \"contributor_champion_memberships\".\"enrolled_tier\" IN ('contributor', 'ambassador', 'champion')" + } + }, + "isRLSEnabled": false + }, + "public.contributor_champion_sync_state": { + "name": "contributor_champion_sync_state", + "schema": "", + "columns": { + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_merged_at": { + "name": "last_merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_insight_active_suggestions": { + "name": "cost_insight_active_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "suggestion_kind": { + "name": "suggestion_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggestion_key": { + "name": "suggestion_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta_label": { + "name": "cta_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta_href": { + "name": "cta_href", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_window_start": { + "name": "evidence_window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "evidence_window_end": { + "name": "evidence_window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "observed_microdollars": { + "name": "observed_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "benefit_label": { + "name": "benefit_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "benefit_detail": { + "name": "benefit_detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_by_user_id": { + "name": "dismissed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cost_insight_active_suggestions_user_key": { + "name": "UQ_cost_insight_active_suggestions_user_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "suggestion_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_active_suggestions\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_active_suggestions_org_key": { + "name": "UQ_cost_insight_active_suggestions_org_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "suggestion_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_active_suggestions\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_active_suggestions_user_active": { + "name": "IDX_cost_insight_active_suggestions_user_active", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_active_suggestions\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_active_suggestions\".\"dismissed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_active_suggestions_org_active": { + "name": "IDX_cost_insight_active_suggestions_org_active", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_active_suggestions\".\"owned_by_organization_id\" IS NOT NULL AND \"cost_insight_active_suggestions\".\"dismissed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_active_suggestions_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_active_suggestions_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_active_suggestions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_active_suggestions_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_active_suggestions_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_active_suggestions", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_active_suggestions_dismissed_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_active_suggestions_dismissed_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_active_suggestions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "dismissed_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_active_suggestions_owner_check": { + "name": "cost_insight_active_suggestions_owner_check", + "value": "(\"cost_insight_active_suggestions\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_active_suggestions\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_active_suggestions\".\"owned_by_user_id\" IS NULL AND \"cost_insight_active_suggestions\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_active_suggestions_kind_check": { + "name": "cost_insight_active_suggestions_kind_check", + "value": "\"cost_insight_active_suggestions\".\"suggestion_kind\" IN ('coding_plan', 'kilo_pass')" + }, + "cost_insight_active_suggestions_key_check": { + "name": "cost_insight_active_suggestions_key_check", + "value": "\"cost_insight_active_suggestions\".\"suggestion_key\" ~ '^[0-9a-f]{64}$'" + }, + "cost_insight_active_suggestions_window_check": { + "name": "cost_insight_active_suggestions_window_check", + "value": "\"cost_insight_active_suggestions\".\"evidence_window_end\" > \"cost_insight_active_suggestions\".\"evidence_window_start\"" + }, + "cost_insight_active_suggestions_observed_positive_check": { + "name": "cost_insight_active_suggestions_observed_positive_check", + "value": "\"cost_insight_active_suggestions\".\"observed_microdollars\" > 0" + }, + "cost_insight_active_suggestions_observed_safe_check": { + "name": "cost_insight_active_suggestions_observed_safe_check", + "value": "\"cost_insight_active_suggestions\".\"observed_microdollars\" <= 9007199254740991" + }, + "cost_insight_active_suggestions_dismissed_by_check": { + "name": "cost_insight_active_suggestions_dismissed_by_check", + "value": "\"cost_insight_active_suggestions\".\"dismissed_at\" IS NOT NULL OR \"cost_insight_active_suggestions\".\"dismissed_by_user_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_events": { + "name": "cost_insight_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alert_kind": { + "name": "alert_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggestion_kind": { + "name": "suggestion_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_suggestion_id": { + "name": "active_suggestion_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cost_insight_events_user_occurred": { + "name": "IDX_cost_insight_events_user_occurred", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_events_org_occurred": { + "name": "IDX_cost_insight_events_org_occurred", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_events_occurred": { + "name": "IDX_cost_insight_events_occurred", + "columns": [ + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_events_user_dedupe": { + "name": "UQ_cost_insight_events_user_dedupe", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_events\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_events\".\"dedupe_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_events_org_dedupe": { + "name": "UQ_cost_insight_events_org_dedupe", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_events\".\"owned_by_organization_id\" IS NOT NULL AND \"cost_insight_events\".\"dedupe_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_events_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_events_active_suggestion_id_cost_insight_active_suggestions_id_fk": { + "name": "cost_insight_events_active_suggestion_id_cost_insight_active_suggestions_id_fk", + "tableFrom": "cost_insight_events", + "tableTo": "cost_insight_active_suggestions", + "columnsFrom": [ + "active_suggestion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_insight_events_actor_user_id_kilocode_users_id_fk": { + "name": "cost_insight_events_actor_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_events_owner_check": { + "name": "cost_insight_events_owner_check", + "value": "(\"cost_insight_events\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_events\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_events\".\"owned_by_user_id\" IS NULL AND \"cost_insight_events\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_events_type_check": { + "name": "cost_insight_events_type_check", + "value": "\"cost_insight_events\".\"event_type\" IN ('config_changed', 'anomaly_alert', 'threshold_crossed', 'alert_reviewed', 'suggestion_created', 'suggestion_dismissed', 'disabled')" + }, + "cost_insight_events_alert_kind_check": { + "name": "cost_insight_events_alert_kind_check", + "value": "\"cost_insight_events\".\"alert_kind\" IN ('anomaly', 'threshold', 'threshold_7d', 'threshold_30d')" + }, + "cost_insight_events_suggestion_kind_check": { + "name": "cost_insight_events_suggestion_kind_check", + "value": "\"cost_insight_events\".\"suggestion_kind\" IN ('coding_plan', 'kilo_pass')" + }, + "cost_insight_events_alert_kind_presence_check": { + "name": "cost_insight_events_alert_kind_presence_check", + "value": "(\"cost_insight_events\".\"event_type\" IN ('anomaly_alert', 'threshold_crossed', 'alert_reviewed') AND \"cost_insight_events\".\"alert_kind\" IS NOT NULL) OR (\"cost_insight_events\".\"event_type\" NOT IN ('anomaly_alert', 'threshold_crossed', 'alert_reviewed') AND \"cost_insight_events\".\"alert_kind\" IS NULL)" + }, + "cost_insight_events_suggestion_kind_presence_check": { + "name": "cost_insight_events_suggestion_kind_presence_check", + "value": "(\"cost_insight_events\".\"event_type\" IN ('suggestion_created', 'suggestion_dismissed') AND \"cost_insight_events\".\"suggestion_kind\" IS NOT NULL) OR (\"cost_insight_events\".\"event_type\" NOT IN ('suggestion_created', 'suggestion_dismissed') AND \"cost_insight_events\".\"suggestion_kind\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_notification_deliveries": { + "name": "cost_insight_notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_at": { + "name": "failed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cost_insight_notification_deliveries_event_recipient_channel": { + "name": "UQ_cost_insight_notification_deliveries_event_recipient_channel", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_notification_deliveries_claim": { + "name": "IDX_cost_insight_notification_deliveries_claim", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_notification_deliveries_event": { + "name": "IDX_cost_insight_notification_deliveries_event", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_notification_deliveries_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_notification_deliveries_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_notification_deliveries", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cost_insight_notification_deliveries_recipient_user_id_kilocode_users_id_fk": { + "name": "cost_insight_notification_deliveries_recipient_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_notification_deliveries", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_notification_deliveries_channel_check": { + "name": "cost_insight_notification_deliveries_channel_check", + "value": "\"cost_insight_notification_deliveries\".\"channel\" = 'email'" + }, + "cost_insight_notification_deliveries_status_check": { + "name": "cost_insight_notification_deliveries_status_check", + "value": "\"cost_insight_notification_deliveries\".\"status\" IN ('pending', 'sending', 'sent', 'failed', 'skipped')" + }, + "cost_insight_notification_deliveries_attempt_count_check": { + "name": "cost_insight_notification_deliveries_attempt_count_check", + "value": "\"cost_insight_notification_deliveries\".\"attempt_count\" >= 0" + }, + "cost_insight_notification_deliveries_terminal_check": { + "name": "cost_insight_notification_deliveries_terminal_check", + "value": "(\"cost_insight_notification_deliveries\".\"status\" = 'sent' AND \"cost_insight_notification_deliveries\".\"sent_at\" IS NOT NULL) OR (\"cost_insight_notification_deliveries\".\"status\" <> 'sent' AND \"cost_insight_notification_deliveries\".\"sent_at\" IS NULL)" + }, + "cost_insight_notification_deliveries_failure_check": { + "name": "cost_insight_notification_deliveries_failure_check", + "value": "(\"cost_insight_notification_deliveries\".\"status\" = 'failed' AND \"cost_insight_notification_deliveries\".\"failed_at\" IS NOT NULL) OR (\"cost_insight_notification_deliveries\".\"status\" <> 'failed' AND \"cost_insight_notification_deliveries\".\"failed_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_owner_configs": { + "name": "cost_insight_owner_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "spend_alerts_enabled": { + "name": "spend_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "anomaly_alerts_enabled": { + "name": "anomaly_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cost_suggestions_enabled": { + "name": "cost_suggestions_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "spend_threshold_microdollars": { + "name": "spend_threshold_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "spend_7_day_threshold_microdollars": { + "name": "spend_7_day_threshold_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "spend_30_day_threshold_microdollars": { + "name": "spend_30_day_threshold_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "spend_alerts_enabled_at": { + "name": "spend_alerts_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cost_insight_owner_configs_user": { + "name": "UQ_cost_insight_owner_configs_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_configs\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_owner_configs_org": { + "name": "UQ_cost_insight_owner_configs_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_configs\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_configs_evaluation": { + "name": "IDX_cost_insight_owner_configs_evaluation", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_configs\".\"spend_alerts_enabled\" = TRUE OR \"cost_insight_owner_configs\".\"cost_suggestions_enabled\" = TRUE", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_owner_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_configs_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_owner_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_owner_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_owner_configs_owner_check": { + "name": "cost_insight_owner_configs_owner_check", + "value": "(\"cost_insight_owner_configs\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_owner_configs\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_owner_configs\".\"owned_by_user_id\" IS NULL AND \"cost_insight_owner_configs\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_owner_configs_threshold_positive_check": { + "name": "cost_insight_owner_configs_threshold_positive_check", + "value": "\"cost_insight_owner_configs\".\"spend_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_threshold_microdollars\" > 0" + }, + "cost_insight_owner_configs_threshold_safe_check": { + "name": "cost_insight_owner_configs_threshold_safe_check", + "value": "\"cost_insight_owner_configs\".\"spend_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_threshold_microdollars\" <= 9007199254740991" + }, + "cost_insight_owner_configs_7_day_threshold_positive_check": { + "name": "cost_insight_owner_configs_7_day_threshold_positive_check", + "value": "\"cost_insight_owner_configs\".\"spend_7_day_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_7_day_threshold_microdollars\" > 0" + }, + "cost_insight_owner_configs_7_day_threshold_safe_check": { + "name": "cost_insight_owner_configs_7_day_threshold_safe_check", + "value": "\"cost_insight_owner_configs\".\"spend_7_day_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_7_day_threshold_microdollars\" <= 9007199254740991" + }, + "cost_insight_owner_configs_30_day_threshold_positive_check": { + "name": "cost_insight_owner_configs_30_day_threshold_positive_check", + "value": "\"cost_insight_owner_configs\".\"spend_30_day_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_30_day_threshold_microdollars\" > 0" + }, + "cost_insight_owner_configs_30_day_threshold_safe_check": { + "name": "cost_insight_owner_configs_30_day_threshold_safe_check", + "value": "\"cost_insight_owner_configs\".\"spend_30_day_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_30_day_threshold_microdollars\" <= 9007199254740991" + }, + "cost_insight_owner_configs_enabled_at_check": { + "name": "cost_insight_owner_configs_enabled_at_check", + "value": "\"cost_insight_owner_configs\".\"spend_alerts_enabled\" = TRUE OR \"cost_insight_owner_configs\".\"spend_alerts_enabled_at\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_owner_hour_driver_buckets": { + "name": "cost_insight_owner_hour_driver_buckets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "spend_category": { + "name": "spend_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "driver_key": { + "name": "driver_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_key": { + "name": "product_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_key": { + "name": "feature_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_or_plan_key": { + "name": "model_or_plan_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_microdollars": { + "name": "total_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spend_record_count": { + "name": "spend_record_count", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cost_insight_driver_buckets_user": { + "name": "UQ_cost_insight_driver_buckets_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spend_category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "driver_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_hour_driver_buckets\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_driver_buckets_org": { + "name": "UQ_cost_insight_driver_buckets_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spend_category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "driver_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_hour_driver_buckets\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_driver_buckets_hour": { + "name": "IDX_cost_insight_driver_buckets_hour", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_owner_hour_driver_buckets_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_hour_driver_buckets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_hour_driver_buckets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_hour_driver_buckets_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_owner_hour_driver_buckets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_owner_hour_driver_buckets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_hour_driver_buckets_actor_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_hour_driver_buckets_actor_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_hour_driver_buckets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_driver_buckets_owner_check": { + "name": "cost_insight_driver_buckets_owner_check", + "value": "(\"cost_insight_owner_hour_driver_buckets\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_owner_hour_driver_buckets\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_owner_hour_driver_buckets\".\"owned_by_user_id\" IS NULL AND \"cost_insight_owner_hour_driver_buckets\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_driver_buckets_hour_check": { + "name": "cost_insight_driver_buckets_hour_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"hour_start\" = date_trunc('hour', \"cost_insight_owner_hour_driver_buckets\".\"hour_start\", 'UTC')" + }, + "cost_insight_driver_buckets_category_check": { + "name": "cost_insight_driver_buckets_category_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"spend_category\" IN ('variable', 'scheduled')" + }, + "cost_insight_driver_buckets_source_check": { + "name": "cost_insight_driver_buckets_source_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"source\" IN ('ai_gateway', 'kiloclaw', 'coding_plan', 'other')" + }, + "cost_insight_driver_buckets_driver_key_check": { + "name": "cost_insight_driver_buckets_driver_key_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"driver_key\" ~ '^[0-9a-f]{64}$'" + }, + "cost_insight_driver_buckets_product_key_check": { + "name": "cost_insight_driver_buckets_product_key_check", + "value": "char_length(\"cost_insight_owner_hour_driver_buckets\".\"product_key\") BETWEEN 1 AND 128" + }, + "cost_insight_driver_buckets_feature_key_check": { + "name": "cost_insight_driver_buckets_feature_key_check", + "value": "char_length(\"cost_insight_owner_hour_driver_buckets\".\"feature_key\") BETWEEN 1 AND 128" + }, + "cost_insight_driver_buckets_model_key_check": { + "name": "cost_insight_driver_buckets_model_key_check", + "value": "char_length(\"cost_insight_owner_hour_driver_buckets\".\"model_or_plan_key\") BETWEEN 1 AND 128" + }, + "cost_insight_driver_buckets_provider_key_check": { + "name": "cost_insight_driver_buckets_provider_key_check", + "value": "char_length(\"cost_insight_owner_hour_driver_buckets\".\"provider_key\") BETWEEN 1 AND 128" + }, + "cost_insight_driver_buckets_amount_positive_check": { + "name": "cost_insight_driver_buckets_amount_positive_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"total_microdollars\" > 0" + }, + "cost_insight_driver_buckets_amount_safe_check": { + "name": "cost_insight_driver_buckets_amount_safe_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"total_microdollars\" <= 9007199254740991" + }, + "cost_insight_driver_buckets_count_positive_check": { + "name": "cost_insight_driver_buckets_count_positive_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"spend_record_count\" > 0" + }, + "cost_insight_driver_buckets_count_safe_check": { + "name": "cost_insight_driver_buckets_count_safe_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"spend_record_count\" <= 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_owner_hour_totals": { + "name": "cost_insight_owner_hour_totals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "spend_category": { + "name": "spend_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_microdollars": { + "name": "total_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spend_record_count": { + "name": "spend_record_count", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cost_insight_owner_hour_totals_user": { + "name": "UQ_cost_insight_owner_hour_totals_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spend_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_hour_totals\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_owner_hour_totals_org": { + "name": "UQ_cost_insight_owner_hour_totals_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spend_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_hour_totals\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_hour_totals_hour": { + "name": "IDX_cost_insight_owner_hour_totals_hour", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_owner_hour_totals_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_hour_totals_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_hour_totals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_hour_totals_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_owner_hour_totals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_owner_hour_totals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_owner_hour_totals_owner_check": { + "name": "cost_insight_owner_hour_totals_owner_check", + "value": "(\"cost_insight_owner_hour_totals\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_owner_hour_totals\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_owner_hour_totals\".\"owned_by_user_id\" IS NULL AND \"cost_insight_owner_hour_totals\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_owner_hour_totals_hour_check": { + "name": "cost_insight_owner_hour_totals_hour_check", + "value": "\"cost_insight_owner_hour_totals\".\"hour_start\" = date_trunc('hour', \"cost_insight_owner_hour_totals\".\"hour_start\", 'UTC')" + }, + "cost_insight_owner_hour_totals_category_check": { + "name": "cost_insight_owner_hour_totals_category_check", + "value": "\"cost_insight_owner_hour_totals\".\"spend_category\" IN ('variable', 'scheduled')" + }, + "cost_insight_owner_hour_totals_amount_positive_check": { + "name": "cost_insight_owner_hour_totals_amount_positive_check", + "value": "\"cost_insight_owner_hour_totals\".\"total_microdollars\" > 0" + }, + "cost_insight_owner_hour_totals_amount_safe_check": { + "name": "cost_insight_owner_hour_totals_amount_safe_check", + "value": "\"cost_insight_owner_hour_totals\".\"total_microdollars\" <= 9007199254740991" + }, + "cost_insight_owner_hour_totals_count_positive_check": { + "name": "cost_insight_owner_hour_totals_count_positive_check", + "value": "\"cost_insight_owner_hour_totals\".\"spend_record_count\" > 0" + }, + "cost_insight_owner_hour_totals_count_safe_check": { + "name": "cost_insight_owner_hour_totals_count_safe_check", + "value": "\"cost_insight_owner_hour_totals\".\"spend_record_count\" <= 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_owner_states": { + "name": "cost_insight_owner_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_anomaly_event_id": { + "name": "active_anomaly_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_anomaly_hour_start": { + "name": "active_anomaly_hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_anomaly_reviewed_at": { + "name": "active_anomaly_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "threshold_crossing_active": { + "name": "threshold_crossing_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_threshold_event_id": { + "name": "active_threshold_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "threshold_crossing_started_at": { + "name": "threshold_crossing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "threshold_reviewed_at": { + "name": "threshold_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "threshold_recovered_at": { + "name": "threshold_recovered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_7_day_threshold_crossing_active": { + "name": "rolling_7_day_threshold_crossing_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_rolling_7_day_threshold_event_id": { + "name": "active_rolling_7_day_threshold_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rolling_7_day_threshold_crossing_started_at": { + "name": "rolling_7_day_threshold_crossing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_7_day_threshold_reviewed_at": { + "name": "rolling_7_day_threshold_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_7_day_threshold_recovered_at": { + "name": "rolling_7_day_threshold_recovered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_30_day_threshold_crossing_active": { + "name": "rolling_30_day_threshold_crossing_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_rolling_30_day_threshold_event_id": { + "name": "active_rolling_30_day_threshold_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rolling_30_day_threshold_crossing_started_at": { + "name": "rolling_30_day_threshold_crossing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_30_day_threshold_reviewed_at": { + "name": "rolling_30_day_threshold_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_30_day_threshold_recovered_at": { + "name": "rolling_30_day_threshold_recovered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cost_insight_owner_states_user": { + "name": "UQ_cost_insight_owner_states_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_states\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_owner_states_org": { + "name": "UQ_cost_insight_owner_states_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_states\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_states_unreviewed_user": { + "name": "IDX_cost_insight_owner_states_unreviewed_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_states\".\"owned_by_user_id\" IS NOT NULL AND ((\"cost_insight_owner_states\".\"active_anomaly_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"active_anomaly_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL))", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_states_unreviewed_org": { + "name": "IDX_cost_insight_owner_states_unreviewed_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_states\".\"owned_by_organization_id\" IS NOT NULL AND ((\"cost_insight_owner_states\".\"active_anomaly_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"active_anomaly_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL))", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_owner_states_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_states_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_states_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_owner_states_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_states_active_anomaly_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_owner_states_active_anomaly_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "active_anomaly_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_insight_owner_states_active_threshold_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_owner_states_active_threshold_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "active_threshold_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_insight_owner_states_active_rolling_7_day_threshold_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_owner_states_active_rolling_7_day_threshold_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "active_rolling_7_day_threshold_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_insight_owner_states_active_rolling_30_day_threshold_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_owner_states_active_rolling_30_day_threshold_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "active_rolling_30_day_threshold_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_owner_states_owner_check": { + "name": "cost_insight_owner_states_owner_check", + "value": "(\"cost_insight_owner_states\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_owner_states\".\"owned_by_user_id\" IS NULL AND \"cost_insight_owner_states\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_owner_states_anomaly_hour_check": { + "name": "cost_insight_owner_states_anomaly_hour_check", + "value": "\"cost_insight_owner_states\".\"active_anomaly_hour_start\" IS NULL OR \"cost_insight_owner_states\".\"active_anomaly_hour_start\" = date_trunc('hour', \"cost_insight_owner_states\".\"active_anomaly_hour_start\", 'UTC')" + }, + "cost_insight_owner_states_threshold_active_check": { + "name": "cost_insight_owner_states_threshold_active_check", + "value": "\"cost_insight_owner_states\".\"threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL)" + }, + "cost_insight_owner_states_7_day_threshold_active_check": { + "name": "cost_insight_owner_states_7_day_threshold_active_check", + "value": "\"cost_insight_owner_states\".\"rolling_7_day_threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL)" + }, + "cost_insight_owner_states_30_day_threshold_active_check": { + "name": "cost_insight_owner_states_30_day_threshold_active_check", + "value": "\"cost_insight_owner_states\".\"rolling_30_day_threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_rollup_coverage": { + "name": "cost_insight_rollup_coverage", + "schema": "", + "columns": { + "rollup_version": { + "name": "rollup_version", + "type": "smallint", + "primaryKey": true, + "notNull": true + }, + "live_capture_start_hour": { + "name": "live_capture_start_hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "coverage_start_hour": { + "name": "coverage_start_hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_reconciled_at": { + "name": "last_reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_rollup_coverage_version_check": { + "name": "cost_insight_rollup_coverage_version_check", + "value": "\"cost_insight_rollup_coverage\".\"rollup_version\" > 0" + }, + "cost_insight_rollup_coverage_live_hour_check": { + "name": "cost_insight_rollup_coverage_live_hour_check", + "value": "\"cost_insight_rollup_coverage\".\"live_capture_start_hour\" IS NULL OR \"cost_insight_rollup_coverage\".\"live_capture_start_hour\" = date_trunc('hour', \"cost_insight_rollup_coverage\".\"live_capture_start_hour\", 'UTC')" + }, + "cost_insight_rollup_coverage_start_hour_check": { + "name": "cost_insight_rollup_coverage_start_hour_check", + "value": "\"cost_insight_rollup_coverage\".\"coverage_start_hour\" IS NULL OR \"cost_insight_rollup_coverage\".\"coverage_start_hour\" = date_trunc('hour', \"cost_insight_rollup_coverage\".\"coverage_start_hour\", 'UTC')" + }, + "cost_insight_rollup_coverage_range_check": { + "name": "cost_insight_rollup_coverage_range_check", + "value": "\"cost_insight_rollup_coverage\".\"coverage_start_hour\" IS NULL OR (\"cost_insight_rollup_coverage\".\"live_capture_start_hour\" IS NOT NULL AND \"cost_insight_rollup_coverage\".\"coverage_start_hour\" <= \"cost_insight_rollup_coverage\".\"live_capture_start_hour\")" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_rollup_degraded_intervals": { + "name": "cost_insight_rollup_degraded_intervals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "start_hour": { + "name": "start_hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_hour_exclusive": { + "name": "end_hour_exclusive", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_at": { + "name": "detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cost_insight_degraded_intervals_unresolved": { + "name": "IDX_cost_insight_degraded_intervals_unresolved", + "columns": [ + { + "expression": "start_hour", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "end_hour_exclusive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_rollup_degraded_intervals\".\"resolved_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_degraded_intervals_start_hour_check": { + "name": "cost_insight_degraded_intervals_start_hour_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"start_hour\" = date_trunc('hour', \"cost_insight_rollup_degraded_intervals\".\"start_hour\", 'UTC')" + }, + "cost_insight_degraded_intervals_end_hour_check": { + "name": "cost_insight_degraded_intervals_end_hour_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"end_hour_exclusive\" = date_trunc('hour', \"cost_insight_rollup_degraded_intervals\".\"end_hour_exclusive\", 'UTC')" + }, + "cost_insight_degraded_intervals_range_check": { + "name": "cost_insight_degraded_intervals_range_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"end_hour_exclusive\" > \"cost_insight_rollup_degraded_intervals\".\"start_hour\"" + }, + "cost_insight_degraded_intervals_resolution_check": { + "name": "cost_insight_degraded_intervals_resolution_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"resolved_at\" IS NULL OR \"cost_insight_rollup_degraded_intervals\".\"resolved_at\" >= \"cost_insight_rollup_degraded_intervals\".\"detected_at\"" + }, + "cost_insight_degraded_intervals_source_check": { + "name": "cost_insight_degraded_intervals_source_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"source\" IN ('ai_gateway', 'kiloclaw', 'coding_plan', 'other')" + }, + "cost_insight_degraded_intervals_reason_check": { + "name": "cost_insight_degraded_intervals_reason_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"reason\" IN ('capture_bypass', 'reconciliation_mismatch', 'late_source_data')" + } + }, + "isRLSEnabled": false + }, + "public.credit_campaigns": { + "name": "credit_campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_expiry_hours": { + "name": "credit_expiry_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_ends_at": { + "name": "campaign_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_redemptions_allowed": { + "name": "total_redemptions_allowed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_credit_campaigns_slug": { + "name": "UQ_credit_campaigns_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_credit_campaigns_credit_category": { + "name": "UQ_credit_campaigns_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credit_campaigns_slug_format_check": { + "name": "credit_campaigns_slug_format_check", + "value": "\"credit_campaigns\".\"slug\" ~ '^[a-z0-9-]{5,40}$'" + }, + "credit_campaigns_amount_positive_check": { + "name": "credit_campaigns_amount_positive_check", + "value": "\"credit_campaigns\".\"amount_microdollars\" > 0" + }, + "credit_campaigns_credit_expiry_hours_positive_check": { + "name": "credit_campaigns_credit_expiry_hours_positive_check", + "value": "\"credit_campaigns\".\"credit_expiry_hours\" IS NULL OR \"credit_campaigns\".\"credit_expiry_hours\" > 0" + }, + "credit_campaigns_total_redemptions_allowed_positive_check": { + "name": "credit_campaigns_total_redemptions_allowed_positive_check", + "value": "\"credit_campaigns\".\"total_redemptions_allowed\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.credit_transactions": { + "name": "credit_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expiration_baseline_microdollars_used": { + "name": "expiration_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "original_baseline_microdollars_used": { + "name": "original_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_transaction_id": { + "name": "original_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_id": { + "name": "stripe_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coinbase_credit_block_id": { + "name": "coinbase_credit_block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_date": { + "name": "expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "check_category_uniqueness": { + "name": "check_category_uniqueness", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_credit_transactions_created_at": { + "name": "IDX_credit_transactions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_is_free": { + "name": "IDX_credit_transactions_is_free", + "columns": [ + { + "expression": "is_free", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_kilo_user_id": { + "name": "IDX_credit_transactions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_credit_category": { + "name": "IDX_credit_transactions_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_stripe_payment_id": { + "name": "IDX_credit_transactions_stripe_payment_id", + "columns": [ + { + "expression": "stripe_payment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_original_transaction_id": { + "name": "IDX_credit_transactions_original_transaction_id", + "columns": [ + { + "expression": "original_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_coinbase_credit_block_id": { + "name": "IDX_credit_transactions_coinbase_credit_block_id", + "columns": [ + { + "expression": "coinbase_credit_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_organization_id": { + "name": "IDX_credit_transactions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_unique_category": { + "name": "IDX_credit_transactions_unique_category", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credit_transactions\".\"check_category_uniqueness\" = TRUE", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "credit_transactions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_llm2": { + "name": "custom_llm2", + "schema": "", + "columns": { + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deleted_user_email_tombstones": { + "name": "deleted_user_email_tombstones", + "schema": "", + "columns": { + "normalized_email_hash": { + "name": "normalized_email_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_builds": { + "name": "deployment_builds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_builds_deployment_id": { + "name": "idx_deployment_builds_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_builds_status": { + "name": "idx_deployment_builds_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_builds_deployment_id_deployments_id_fk": { + "name": "deployment_builds_deployment_id_deployments_id_fk", + "tableFrom": "deployment_builds", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_env_vars": { + "name": "deployment_env_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_env_vars_deployment_id": { + "name": "idx_deployment_env_vars_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_env_vars_deployment_id_deployments_id_fk": { + "name": "deployment_env_vars_deployment_id_deployments_id_fk", + "tableFrom": "deployment_env_vars", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployment_env_vars_deployment_key": { + "name": "UQ_deployment_env_vars_deployment_key", + "nullsNotDistinct": false, + "columns": [ + "deployment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_events": { + "name": "deployment_events", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'log'" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_deployment_events_build_id": { + "name": "idx_deployment_events_build_id", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_events_timestamp": { + "name": "idx_deployment_events_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_events_type": { + "name": "idx_deployment_events_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_events_build_id_deployment_builds_id_fk": { + "name": "deployment_events_build_id_deployment_builds_id_fk", + "tableFrom": "deployment_events", + "tableTo": "deployment_builds", + "columnsFrom": [ + "build_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_events_build_id_event_id_pk": { + "name": "deployment_events_build_id_event_id_pk", + "columns": [ + "build_id", + "event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_threat_detections": { + "name": "deployment_threat_detections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "build_id": { + "name": "build_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "threat_type": { + "name": "threat_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_threat_detections_deployment_id": { + "name": "idx_deployment_threat_detections_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_threat_detections_created_at": { + "name": "idx_deployment_threat_detections_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_threat_detections_deployment_id_deployments_id_fk": { + "name": "deployment_threat_detections_deployment_id_deployments_id_fk", + "tableFrom": "deployment_threat_detections", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_threat_detections_build_id_deployment_builds_id_fk": { + "name": "deployment_threat_detections_build_id_deployment_builds_id_fk", + "tableFrom": "deployment_threat_detections", + "tableTo": "deployment_builds", + "columnsFrom": [ + "build_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployments": { + "name": "deployments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deployment_slug": { + "name": "deployment_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_worker_name": { + "name": "internal_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_source": { + "name": "repository_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_url": { + "name": "deployment_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "git_auth_token": { + "name": "git_auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_deployed_at": { + "name": "last_deployed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_build_id": { + "name": "last_build_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "threat_status": { + "name": "threat_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_from": { + "name": "created_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_deployments_owned_by_user_id": { + "name": "idx_deployments_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_owned_by_organization_id": { + "name": "idx_deployments_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_platform_integration_id": { + "name": "idx_deployments_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_repository_source_branch": { + "name": "idx_deployments_repository_source_branch", + "columns": [ + { + "expression": "repository_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_threat_status_pending": { + "name": "idx_deployments_threat_status_pending", + "columns": [ + { + "expression": "threat_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"deployments\".\"threat_status\" = 'pending_scan'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_owned_by_user_id_kilocode_users_id_fk": { + "name": "deployments_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "deployments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployments_owned_by_organization_id_organizations_id_fk": { + "name": "deployments_owned_by_organization_id_organizations_id_fk", + "tableFrom": "deployments", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployments_deployment_slug": { + "name": "UQ_deployments_deployment_slug", + "nullsNotDistinct": false, + "columns": [ + "deployment_slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "deployments_owner_check": { + "name": "deployments_owner_check", + "value": "(\n (\"deployments\".\"owned_by_user_id\" IS NOT NULL AND \"deployments\".\"owned_by_organization_id\" IS NULL) OR\n (\"deployments\".\"owned_by_user_id\" IS NULL AND \"deployments\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "deployments_source_type_check": { + "name": "deployments_source_type_check", + "value": "\"deployments\".\"source_type\" IN ('github', 'git', 'app-builder')" + } + }, + "isRLSEnabled": false + }, + "public.deployments_ephemeral": { + "name": "deployments_ephemeral", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_worker_name": { + "name": "internal_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_slug": { + "name": "deployment_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_cleanup_at": { + "name": "next_cleanup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cleanup_claim_token": { + "name": "cleanup_claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cleanup_claimed_until": { + "name": "cleanup_claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployments_ephemeral_owned_by_user_id": { + "name": "idx_deployments_ephemeral_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_ephemeral_next_cleanup_at": { + "name": "idx_deployments_ephemeral_next_cleanup_at", + "columns": [ + { + "expression": "next_cleanup_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_ephemeral_owned_by_user_id_kilocode_users_id_fk": { + "name": "deployments_ephemeral_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "deployments_ephemeral", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployments_ephemeral_internal_worker_name": { + "name": "UQ_deployments_ephemeral_internal_worker_name", + "nullsNotDistinct": false, + "columns": [ + "internal_worker_name" + ] + }, + "UQ_deployments_ephemeral_deployment_slug": { + "name": "UQ_deployments_ephemeral_deployment_slug", + "nullsNotDistinct": false, + "columns": [ + "deployment_slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "deployments_ephemeral_source_type_check": { + "name": "deployments_ephemeral_source_type_check", + "value": "\"deployments_ephemeral\".\"source_type\" IN ('html')" + }, + "deployments_ephemeral_status_check": { + "name": "deployments_ephemeral_status_check", + "value": "\"deployments_ephemeral\".\"status\" IN ('pending', 'active', 'cleanup_retry')" + }, + "deployments_ephemeral_claim_fields_check": { + "name": "deployments_ephemeral_claim_fields_check", + "value": "(\"deployments_ephemeral\".\"cleanup_claim_token\" IS NULL) = (\"deployments_ephemeral\".\"cleanup_claimed_until\" IS NULL)" + }, + "deployments_ephemeral_active_fields_check": { + "name": "deployments_ephemeral_active_fields_check", + "value": "\"deployments_ephemeral\".\"status\" <> 'active' OR (\"deployments_ephemeral\".\"deployment_slug\" IS NOT NULL AND \"deployments_ephemeral\".\"expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.device_auth_requests": { + "name": "device_auth_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_device_auth_requests_code": { + "name": "UQ_device_auth_requests_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_status": { + "name": "IDX_device_auth_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_expires_at": { + "name": "IDX_device_auth_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_kilo_user_id": { + "name": "IDX_device_auth_requests_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_auth_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "device_auth_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_auth_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord_gateway_listener": { + "name": "discord_gateway_listener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "listener_id": { + "name": "listener_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.editor_name": { + "name": "editor_name", + "schema": "", + "columns": { + "editor_name_id": { + "name": "editor_name_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_editor_name": { + "name": "UQ_editor_name", + "columns": [ + { + "expression": "editor_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.enrichment_data": { + "name": "enrichment_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_enrichment_data": { + "name": "github_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linkedin_enrichment_data": { + "name": "linkedin_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "clay_enrichment_data": { + "name": "clay_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_enrichment_data_user_id": { + "name": "IDX_enrichment_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "enrichment_data_user_id_kilocode_users_id_fk": { + "name": "enrichment_data_user_id_kilocode_users_id_fk", + "tableFrom": "enrichment_data", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_enrichment_data_user_id": { + "name": "UQ_enrichment_data_user_id", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_monthly_usage": { + "name": "exa_monthly_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "month": { + "name": "month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_charged_microdollars": { + "name": "total_charged_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "free_allowance_microdollars": { + "name": "free_allowance_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 10000000 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_monthly_usage_personal": { + "name": "idx_exa_monthly_usage_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_exa_monthly_usage_org": { + "name": "idx_exa_monthly_usage_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_usage_log": { + "name": "exa_usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "charged_to_balance": { + "name": "charged_to_balance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_usage_log_user_created": { + "name": "idx_exa_usage_log_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "exa_usage_log_id_created_at_pk": { + "name": "exa_usage_log_id_created_at_pk", + "columns": [ + "id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feature": { + "name": "feature", + "schema": "", + "columns": { + "feature_id": { + "name": "feature_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_feature": { + "name": "UQ_feature", + "columns": [ + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finish_reason": { + "name": "finish_reason", + "schema": "", + "columns": { + "finish_reason_id": { + "name": "finish_reason_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_finish_reason": { + "name": "UQ_finish_reason", + "columns": [ + { + "expression": "finish_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_model_usage": { + "name": "free_model_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_model_usage_ip_created_at": { + "name": "idx_free_model_usage_ip_created_at", + "columns": [ + { + "expression": "ip_address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_model_usage_created_at": { + "name": "idx_free_model_usage_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_branch_pull_requests": { + "name": "github_branch_pull_requests", + "schema": "", + "columns": { + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_state": { + "name": "pr_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_sha": { + "name": "pr_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_review_decision": { + "name": "pr_review_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_decision_pending": { + "name": "review_decision_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "review_decision_fetching_at": { + "name": "review_decision_fetching_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_last_synced_at": { + "name": "pr_last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_github_branch_prs_org": { + "name": "UQ_github_branch_prs_org", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_github_branch_prs_user": { + "name": "UQ_github_branch_prs_user", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk": { + "name": "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_branch_pull_requests_owner_check": { + "name": "github_branch_pull_requests_owner_check", + "value": "(\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NOT NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NULL) OR\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NOT NULL)\n )" + }, + "github_branch_pull_requests_review_decision_check": { + "name": "github_branch_pull_requests_review_decision_check", + "value": "\"github_branch_pull_requests\".\"pr_review_decision\" IS NULL OR \"github_branch_pull_requests\".\"pr_review_decision\" IN ('approved', 'changes_requested', 'review_required')" + } + }, + "isRLSEnabled": false + }, + "public.http_ip": { + "name": "http_ip", + "schema": "", + "columns": { + "http_ip_id": { + "name": "http_ip_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_ip": { + "name": "http_ip", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_ip": { + "name": "UQ_http_ip", + "columns": [ + { + "expression": "http_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.http_user_agent": { + "name": "http_user_agent", + "schema": "", + "columns": { + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_user_agent": { + "name": "UQ_http_user_agent", + "columns": [ + { + "expression": "http_user_agent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.impact_advocate_participants": { + "name": "impact_advocate_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_id": { + "name": "advocate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_account_id": { + "name": "advocate_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_referral_identifier": { + "name": "opaque_referral_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_state": { + "name": "registration_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_registration_attempt_at": { + "name": "last_registration_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_impact_advocate_participants_program_referral_identifier": { + "name": "UQ_impact_advocate_participants_program_referral_identifier", + "columns": [ + { + "expression": "program_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opaque_referral_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"impact_advocate_participants\".\"opaque_referral_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_participants_registration_state": { + "name": "IDX_impact_advocate_participants_registration_state", + "columns": [ + { + "expression": "registration_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_participants_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_participants_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_participants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_participants_program_user": { + "name": "UQ_impact_advocate_participants_program_user", + "nullsNotDistinct": false, + "columns": [ + "program_key", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_participants_program_key_check": { + "name": "impact_advocate_participants_program_key_check", + "value": "\"impact_advocate_participants\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_participants_registration_state_check": { + "name": "impact_advocate_participants_registration_state_check", + "value": "\"impact_advocate_participants\".\"registration_state\" IN ('pending', 'retrying', 'registered', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_registration_attempts": { + "name": "impact_advocate_registration_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_cookie_value": { + "name": "opaque_cookie_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cookie_value_length": { + "name": "cookie_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_advocate_registration_attempts_participant_id": { + "name": "IDX_impact_advocate_registration_attempts_participant_id", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_registration_attempts_delivery_state": { + "name": "IDX_impact_advocate_registration_attempts_delivery_state", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk": { + "name": "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk", + "tableFrom": "impact_advocate_registration_attempts", + "tableTo": "impact_advocate_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_registration_attempts_dedupe_key": { + "name": "UQ_impact_advocate_registration_attempts_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_registration_attempts_program_key_check": { + "name": "impact_advocate_registration_attempts_program_key_check", + "value": "\"impact_advocate_registration_attempts\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_registration_attempts_delivery_state_check": { + "name": "impact_advocate_registration_attempts_delivery_state_check", + "value": "\"impact_advocate_registration_attempts\".\"delivery_state\" IN ('queued', 'sending', 'succeeded', 'failed')" + }, + "impact_advocate_registration_attempts_cookie_value_length_non_negative_check": { + "name": "impact_advocate_registration_attempts_cookie_value_length_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"cookie_value_length\" >= 0" + }, + "impact_advocate_registration_attempts_attempt_count_non_negative_check": { + "name": "impact_advocate_registration_attempts_attempt_count_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_reward_redemptions": { + "name": "impact_advocate_reward_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "impact_reward_id": { + "name": "impact_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lookup_response_payload": { + "name": "lookup_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "redeem_response_payload": { + "name": "redeem_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_advocate_reward_redemptions_beneficiary_user_id": { + "name": "IDX_impact_advocate_reward_redemptions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_reward_redemptions_state": { + "name": "IDX_impact_advocate_reward_redemptions_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_reward_redemptions_reward_id": { + "name": "UQ_impact_advocate_reward_redemptions_reward_id", + "nullsNotDistinct": false, + "columns": [ + "reward_id" + ] + }, + "UQ_impact_advocate_reward_redemptions_dedupe_key": { + "name": "UQ_impact_advocate_reward_redemptions_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_reward_redemptions_state_check": { + "name": "impact_advocate_reward_redemptions_state_check", + "value": "\"impact_advocate_reward_redemptions\".\"state\" IN ('queued', 'retrying', 'redeemed', 'failed')" + }, + "impact_advocate_reward_redemptions_attempt_count_non_negative_check": { + "name": "impact_advocate_reward_redemptions_attempt_count_non_negative_check", + "value": "\"impact_advocate_reward_redemptions\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_attribution_touches": { + "name": "impact_attribution_touches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'kiloclaw'" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anonymous_id": { + "name": "anonymous_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touch_type": { + "name": "touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_tracking_value": { + "name": "opaque_tracking_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_value_length": { + "name": "tracking_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_tracking_value_accepted": { + "name": "is_tracking_value_accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rs_code": { + "name": "rs_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_share_medium": { + "name": "rs_share_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_engagement_medium": { + "name": "rs_engagement_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "im_ref": { + "name": "im_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_path": { + "name": "landing_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touched_at": { + "name": "touched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sale_attributed_at": { + "name": "sale_attributed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_attribution_touches_product_user_id": { + "name": "IDX_impact_attribution_touches_product_user_id", + "columns": [ + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_user_id": { + "name": "IDX_impact_attribution_touches_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_anonymous_id": { + "name": "IDX_impact_attribution_touches_anonymous_id", + "columns": [ + { + "expression": "anonymous_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_expires_at": { + "name": "IDX_impact_attribution_touches_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_sale_attributed_at": { + "name": "IDX_impact_attribution_touches_sale_attributed_at", + "columns": [ + { + "expression": "sale_attributed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_attribution_touches_user_id_kilocode_users_id_fk": { + "name": "impact_attribution_touches_user_id_kilocode_users_id_fk", + "tableFrom": "impact_attribution_touches", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_attribution_touches_dedupe_key": { + "name": "UQ_impact_attribution_touches_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_attribution_touches_product_check": { + "name": "impact_attribution_touches_product_check", + "value": "\"impact_attribution_touches\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_program_key_check": { + "name": "impact_attribution_touches_program_key_check", + "value": "\"impact_attribution_touches\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_touch_type_check": { + "name": "impact_attribution_touches_touch_type_check", + "value": "\"impact_attribution_touches\".\"touch_type\" IN ('affiliate', 'referral')" + }, + "impact_attribution_touches_provider_check": { + "name": "impact_attribution_touches_provider_check", + "value": "\"impact_attribution_touches\".\"provider\" IN ('impact_performance', 'impact_advocate')" + }, + "impact_attribution_touches_tracking_value_length_non_negative_check": { + "name": "impact_attribution_touches_tracking_value_length_non_negative_check", + "value": "\"impact_attribution_touches\".\"tracking_value_length\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_conversion_reports": { + "name": "impact_conversion_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_tracker_id": { + "name": "action_tracker_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order_id": { + "name": "order_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_conversion_reports_conversion_id": { + "name": "IDX_impact_conversion_reports_conversion_id", + "columns": [ + { + "expression": "conversion_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_conversion_reports_state": { + "name": "IDX_impact_conversion_reports_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_conversion_reports", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_conversion_reports_dedupe_key": { + "name": "UQ_impact_conversion_reports_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_conversion_reports_state_check": { + "name": "impact_conversion_reports_state_check", + "value": "\"impact_conversion_reports\".\"state\" IN ('queued', 'retrying', 'delivered', 'failed')" + }, + "impact_conversion_reports_attempt_count_non_negative_check": { + "name": "impact_conversion_reports_attempt_count_non_negative_check", + "value": "\"impact_conversion_reports\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_conversions": { + "name": "impact_referral_conversions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winning_touch_type": { + "name": "winning_touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credits'" + }, + "source_payment_id": { + "name": "source_payment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qualified": { + "name": "qualified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disqualification_reason": { + "name": "disqualification_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "converted_at": { + "name": "converted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_conversions_referee_user_id": { + "name": "IDX_impact_referral_conversions_referee_user_id", + "columns": [ + { + "expression": "referee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_conversions_referrer_user_id": { + "name": "IDX_impact_referral_conversions_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_conversions_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_conversions_product_payment_source": { + "name": "UQ_impact_referral_conversions_product_payment_source", + "nullsNotDistinct": false, + "columns": [ + "product", + "payment_provider", + "source_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_conversions_product_check": { + "name": "impact_referral_conversions_product_check", + "value": "\"impact_referral_conversions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_conversions_winning_touch_type_check": { + "name": "impact_referral_conversions_winning_touch_type_check", + "value": "\"impact_referral_conversions\".\"winning_touch_type\" IN ('referral', 'affiliate', 'none')" + }, + "impact_referral_conversions_payment_provider_check": { + "name": "impact_referral_conversions_payment_provider_check", + "value": "\"impact_referral_conversions\".\"payment_provider\" IN ('stripe', 'credits', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_applications": { + "name": "impact_referral_reward_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "previous_renewal_boundary": { + "name": "previous_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "new_renewal_boundary": { + "name": "new_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "local_operation_id": { + "name": "local_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_operation_id": { + "name": "stripe_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_idempotency_key": { + "name": "stripe_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_applications_reward_id": { + "name": "IDX_impact_referral_reward_applications_reward_id", + "columns": [ + { + "expression": "reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_reward_applications_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_applications_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_applications_product_check": { + "name": "impact_referral_reward_applications_product_check", + "value": "\"impact_referral_reward_applications\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_decisions": { + "name": "impact_referral_reward_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_decisions_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_decisions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_reward_decisions_conversion_role": { + "name": "UQ_impact_referral_reward_decisions_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_decisions_product_check": { + "name": "impact_referral_reward_decisions_product_check", + "value": "\"impact_referral_reward_decisions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_reward_decisions_beneficiary_role_check": { + "name": "impact_referral_reward_decisions_beneficiary_role_check", + "value": "\"impact_referral_reward_decisions\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_reward_decisions_outcome_check": { + "name": "impact_referral_reward_decisions_outcome_check", + "value": "\"impact_referral_reward_decisions\".\"outcome\" IN ('granted', 'cap_limited', 'disqualified')" + }, + "impact_referral_reward_decisions_reward_kind_check": { + "name": "impact_referral_reward_decisions_reward_kind_check", + "value": "\"impact_referral_reward_decisions\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_reward_decisions_months_granted_non_negative_check": { + "name": "impact_referral_reward_decisions_months_granted_non_negative_check", + "value": "\"impact_referral_reward_decisions\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_rewards": { + "name": "impact_referral_rewards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applies_to_subscription_id": { + "name": "applies_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applies_to_kilo_pass_subscription_id": { + "name": "applies_to_kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_id": { + "name": "consumed_kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_item_id": { + "name": "consumed_kilo_pass_issuance_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "earned_at": { + "name": "earned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reversed_at": { + "name": "reversed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_reason": { + "name": "review_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_rewards_beneficiary_user_id": { + "name": "IDX_impact_referral_rewards_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_rewards_status": { + "name": "IDX_impact_referral_rewards_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk": { + "name": "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_reward_decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_subscription": { + "name": "FK_impact_referral_rewards_kilo_pass_subscription", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "applies_to_kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "consumed_kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance_item": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance_item", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuance_items", + "columnsFrom": [ + "consumed_kilo_pass_issuance_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_rewards_conversion_role": { + "name": "UQ_impact_referral_rewards_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + }, + "UQ_impact_referral_rewards_decision_id": { + "name": "UQ_impact_referral_rewards_decision_id", + "nullsNotDistinct": false, + "columns": [ + "decision_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_rewards_product_check": { + "name": "impact_referral_rewards_product_check", + "value": "\"impact_referral_rewards\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_rewards_beneficiary_role_check": { + "name": "impact_referral_rewards_beneficiary_role_check", + "value": "\"impact_referral_rewards\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_rewards_reward_kind_check": { + "name": "impact_referral_rewards_reward_kind_check", + "value": "\"impact_referral_rewards\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_rewards_status_check": { + "name": "impact_referral_rewards_status_check", + "value": "\"impact_referral_rewards\".\"status\" IN ('pending', 'earned', 'applied', 'reversed', 'expired', 'canceled', 'review_required')" + }, + "impact_referral_rewards_months_granted_non_negative_check": { + "name": "impact_referral_rewards_months_granted_non_negative_check", + "value": "\"impact_referral_rewards\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referrals": { + "name": "impact_referrals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "impact_referral_id": { + "name": "impact_referral_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referrals_referrer_user_id": { + "name": "IDX_impact_referrals_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referrals_source_touch_id": { + "name": "IDX_impact_referrals_source_touch_id", + "columns": [ + { + "expression": "source_touch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referrals_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referrals_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referrals_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referrals_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referrals_product_referee_user_id": { + "name": "UQ_impact_referrals_product_referee_user_id", + "nullsNotDistinct": false, + "columns": [ + "product", + "referee_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referrals_product_check": { + "name": "impact_referrals_product_check", + "value": "\"impact_referrals\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.ja4_digest": { + "name": "ja4_digest", + "schema": "", + "columns": { + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "ja4_digest": { + "name": "ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_ja4_digest": { + "name": "UQ_ja4_digest", + "columns": [ + { + "expression": "ja4_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_audit_log": { + "name": "kilo_pass_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_credit_transaction_id": { + "name": "related_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_monthly_issuance_id": { + "name": "related_monthly_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_kilo_pass_audit_log_created_at": { + "name": "IDX_kilo_pass_audit_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_user_id": { + "name": "IDX_kilo_pass_audit_log_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_pass_subscription_id": { + "name": "IDX_kilo_pass_audit_log_kilo_pass_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_action": { + "name": "IDX_kilo_pass_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_result": { + "name": "IDX_kilo_pass_audit_log_result", + "columns": [ + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_idempotency_key": { + "name": "IDX_kilo_pass_audit_log_idempotency_key", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_event_id": { + "name": "IDX_kilo_pass_audit_log_stripe_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_invoice_id": { + "name": "IDX_kilo_pass_audit_log_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_subscription_id": { + "name": "IDX_kilo_pass_audit_log_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_credit_transaction_id": { + "name": "IDX_kilo_pass_audit_log_related_credit_transaction_id", + "columns": [ + { + "expression": "related_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_monthly_issuance_id": { + "name": "IDX_kilo_pass_audit_log_related_monthly_issuance_id", + "columns": [ + { + "expression": "related_monthly_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "credit_transactions", + "columnsFrom": [ + "related_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "related_monthly_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_audit_log_action_check": { + "name": "kilo_pass_audit_log_action_check", + "value": "\"kilo_pass_audit_log\".\"action\" IN ('stripe_webhook_received', 'kilo_pass_invoice_paid_handled', 'store_purchase_completed', 'store_notification_received', 'store_subscription_renewed', 'store_subscription_canceled', 'store_subscription_expired', 'store_subscription_refunded', 'base_credits_issued', 'bonus_credits_issued', 'bonus_credits_skipped_idempotent', 'first_month_50pct_promo_issued', 'yearly_monthly_base_cron_started', 'yearly_monthly_base_cron_completed', 'issue_yearly_remaining_credits', 'duplicate_card_subscription_canceled', 'yearly_monthly_bonus_cron_started', 'yearly_monthly_bonus_cron_completed')" + }, + "kilo_pass_audit_log_result_check": { + "name": "kilo_pass_audit_log_result_check", + "value": "\"kilo_pass_audit_log\".\"result\" IN ('success', 'skipped_idempotent', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuance_items": { + "name": "kilo_pass_issuance_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_issuance_id": { + "name": "kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "bonus_percent_applied": { + "name": "bonus_percent_applied", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_issuance_items_issuance_id": { + "name": "IDX_kilo_pass_issuance_items_issuance_id", + "columns": [ + { + "expression": "kilo_pass_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuance_items_credit_transaction_id": { + "name": "IDX_kilo_pass_issuance_items_credit_transaction_id", + "columns": [ + { + "expression": "credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_issuance_items_credit_transaction_id_unique": { + "name": "kilo_pass_issuance_items_credit_transaction_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credit_transaction_id" + ] + }, + "UQ_kilo_pass_issuance_items_issuance_kind": { + "name": "UQ_kilo_pass_issuance_items_issuance_kind", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_issuance_id", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuance_items_bonus_percent_applied_range_check": { + "name": "kilo_pass_issuance_items_bonus_percent_applied_range_check", + "value": "\"kilo_pass_issuance_items\".\"bonus_percent_applied\" IS NULL OR (\"kilo_pass_issuance_items\".\"bonus_percent_applied\" >= 0 AND \"kilo_pass_issuance_items\".\"bonus_percent_applied\" <= 1)" + }, + "kilo_pass_issuance_items_amount_usd_non_negative_check": { + "name": "kilo_pass_issuance_items_amount_usd_non_negative_check", + "value": "\"kilo_pass_issuance_items\".\"amount_usd\" >= 0" + }, + "kilo_pass_issuance_items_kind_check": { + "name": "kilo_pass_issuance_items_kind_check", + "value": "\"kilo_pass_issuance_items\".\"kind\" IN ('base', 'bonus', 'promo_first_month_50pct', 'referral_bonus')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuances": { + "name": "kilo_pass_issuances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_month": { + "name": "issue_month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initial_welcome_promo_eligibility_reason": { + "name": "initial_welcome_promo_eligibility_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_issuances_stripe_invoice_id": { + "name": "UQ_kilo_pass_issuances_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_issuances\".\"stripe_invoice_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_subscription_id": { + "name": "IDX_kilo_pass_issuances_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_issue_month": { + "name": "IDX_kilo_pass_issuances_issue_month", + "columns": [ + { + "expression": "issue_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_issuances", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_issuances_subscription_issue_month": { + "name": "UQ_kilo_pass_issuances_subscription_issue_month", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_subscription_id", + "issue_month" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuances_issue_month_day_one_check": { + "name": "kilo_pass_issuances_issue_month_day_one_check", + "value": "EXTRACT(DAY FROM \"kilo_pass_issuances\".\"issue_month\") = 1" + }, + "kilo_pass_issuances_source_check": { + "name": "kilo_pass_issuances_source_check", + "value": "\"kilo_pass_issuances\".\"source\" IN ('stripe_invoice', 'app_store_transaction', 'google_play_transaction', 'cron')" + }, + "kilo_pass_issuances_initial_welcome_promo_reason_check": { + "name": "kilo_pass_issuances_initial_welcome_promo_reason_check", + "value": "\"kilo_pass_issuances\".\"initial_welcome_promo_eligibility_reason\" IN ('first_payment_fingerprint_claim', 'fingerprint_previously_claimed', 'missing_fingerprint', 'no_supported_fingerprint', 'no_positive_settlement', 'settlement_unresolved')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_pause_events": { + "name": "kilo_pass_pause_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resumes_at": { + "name": "resumes_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resumed_at": { + "name": "resumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_pause_events_subscription_id": { + "name": "IDX_kilo_pass_pause_events_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_pause_events_one_open_per_sub": { + "name": "UQ_kilo_pass_pause_events_one_open_per_sub", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_pause_events", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_pause_events_resumed_at_after_paused_at_check": { + "name": "kilo_pass_pause_events_resumed_at_after_paused_at_check", + "value": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL OR \"kilo_pass_pause_events\".\"resumed_at\" >= \"kilo_pass_pause_events\".\"paused_at\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_scheduled_changes": { + "name": "kilo_pass_scheduled_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_tier": { + "name": "from_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_cadence": { + "name": "from_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_tier": { + "name": "to_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_cadence": { + "name": "to_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_scheduled_changes_kilo_user_id": { + "name": "IDX_kilo_pass_scheduled_changes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_status": { + "name": "IDX_kilo_pass_scheduled_changes_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_stripe_subscription_id": { + "name": "IDX_kilo_pass_scheduled_changes_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id": { + "name": "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_scheduled_changes\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_effective_at": { + "name": "IDX_kilo_pass_scheduled_changes_effective_at", + "columns": [ + { + "expression": "effective_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_deleted_at": { + "name": "IDX_kilo_pass_scheduled_changes_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk": { + "name": "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "stripe_subscription_id" + ], + "columnsTo": [ + "stripe_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_scheduled_changes_from_tier_check": { + "name": "kilo_pass_scheduled_changes_from_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_from_cadence_check": { + "name": "kilo_pass_scheduled_changes_from_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_to_tier_check": { + "name": "kilo_pass_scheduled_changes_to_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_to_cadence_check": { + "name": "kilo_pass_scheduled_changes_to_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_status_check": { + "name": "kilo_pass_scheduled_changes_status_check", + "value": "\"kilo_pass_scheduled_changes\".\"status\" IN ('not_started', 'active', 'completed', 'released', 'canceled')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_events": { + "name": "kilo_pass_store_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_store_events_provider_event": { + "name": "UQ_kilo_pass_store_events_provider_event", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_provider_subscription": { + "name": "IDX_kilo_pass_store_events_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_app_account_token": { + "name": "IDX_kilo_pass_store_events_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_events_payment_provider_check": { + "name": "kilo_pass_store_events_payment_provider_check", + "value": "\"kilo_pass_store_events\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_purchases": { + "name": "kilo_pass_store_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_original_transaction_id": { + "name": "provider_original_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purchase_token": { + "name": "purchase_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_at": { + "name": "purchased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "raw_payload_json": { + "name": "raw_payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_store_purchases_provider_transaction": { + "name": "UQ_kilo_pass_store_purchases_provider_transaction", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_subscription_id": { + "name": "IDX_kilo_pass_store_purchases_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_user_id": { + "name": "IDX_kilo_pass_store_purchases_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_app_account_token": { + "name": "IDX_kilo_pass_store_purchases_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_latest_subscription_purchase": { + "name": "IDX_kilo_pass_store_purchases_latest_subscription_purchase", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchased_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_kilo_pass_store_purchases_subscription_owner_provider": { + "name": "FK_kilo_pass_store_purchases_subscription_owner_provider", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "columnsTo": [ + "id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_purchases_store_provider_check": { + "name": "kilo_pass_store_purchases_store_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('app_store', 'google_play')" + }, + "kilo_pass_store_purchases_payment_provider_check": { + "name": "kilo_pass_store_purchases_payment_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_subscriptions": { + "name": "kilo_pass_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stripe'" + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_streak_months": { + "name": "current_streak_months", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_yearly_issue_at": { + "name": "next_yearly_issue_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_subscriptions_kilo_user_id": { + "name": "IDX_kilo_pass_subscriptions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_payment_provider": { + "name": "IDX_kilo_pass_subscriptions_payment_provider", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_status": { + "name": "IDX_kilo_pass_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_cadence": { + "name": "IDX_kilo_pass_subscriptions_cadence", + "columns": [ + { + "expression": "cadence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_provider_subscription": { + "name": "UQ_kilo_pass_subscriptions_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_store_purchase_reference": { + "name": "UQ_kilo_pass_subscriptions_store_purchase_reference", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_subscriptions_stripe_subscription_id_unique": { + "name": "kilo_pass_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_subscriptions_current_streak_months_non_negative_check": { + "name": "kilo_pass_subscriptions_current_streak_months_non_negative_check", + "value": "\"kilo_pass_subscriptions\".\"current_streak_months\" >= 0" + }, + "kilo_pass_subscriptions_provider_ids_check": { + "name": "kilo_pass_subscriptions_provider_ids_check", + "value": "(\n \"kilo_pass_subscriptions\".\"payment_provider\" = 'stripe'\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" = \"kilo_pass_subscriptions\".\"stripe_subscription_id\"\n ) OR (\n \"kilo_pass_subscriptions\".\"payment_provider\" IN ('app_store', 'google_play')\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NULL\n )" + }, + "kilo_pass_subscriptions_payment_provider_check": { + "name": "kilo_pass_subscriptions_payment_provider_check", + "value": "\"kilo_pass_subscriptions\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + }, + "kilo_pass_subscriptions_tier_check": { + "name": "kilo_pass_subscriptions_tier_check", + "value": "\"kilo_pass_subscriptions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_subscriptions_cadence_check": { + "name": "kilo_pass_subscriptions_cadence_check", + "value": "\"kilo_pass_subscriptions\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_welcome_promo_payment_fingerprint_claims": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims", + "schema": "", + "columns": { + "stripe_payment_method_type": { + "name": "stripe_payment_method_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_stripe_invoice_id": { + "name": "source_stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk", + "columns": [ + "stripe_payment_method_type", + "stripe_fingerprint" + ] + } + }, + "uniqueConstraints": { + "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id": { + "name": "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id", + "nullsNotDistinct": false, + "columns": [ + "source_stripe_invoice_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check", + "value": "\"kilo_pass_welcome_promo_payment_fingerprint_claims\".\"stripe_payment_method_type\" IN ('card', 'sepa_debit', 'us_bank_account', 'bacs_debit', 'au_becs_debit')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_access_codes": { + "name": "kiloclaw_access_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_access_codes_code": { + "name": "UQ_kiloclaw_access_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_access_codes_user_status": { + "name": "IDX_kiloclaw_access_codes_user_status", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_access_codes_one_active_per_user": { + "name": "UQ_kiloclaw_access_codes_one_active_per_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_access_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_admin_audit_logs": { + "name": "kiloclaw_admin_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_admin_audit_logs_target_user_id": { + "name": "IDX_kiloclaw_admin_audit_logs_target_user_id", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_action": { + "name": "IDX_kiloclaw_admin_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_created_at": { + "name": "IDX_kiloclaw_admin_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_cli_runs": { + "name": "kiloclaw_cli_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "initiated_by_admin_id": { + "name": "initiated_by_admin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_cli_runs_user_id": { + "name": "IDX_kiloclaw_cli_runs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_started_at": { + "name": "IDX_kiloclaw_cli_runs_started_at", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_instance_id": { + "name": "IDX_kiloclaw_cli_runs_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_cli_runs_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "initiated_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_earlybird_purchases": { + "name": "kiloclaw_earlybird_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manual_payment_id": { + "name": "manual_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_earlybird_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_earlybird_purchases_user_id_unique": { + "name": "kiloclaw_earlybird_purchases_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "kiloclaw_earlybird_purchases_stripe_charge_id_unique": { + "name": "kiloclaw_earlybird_purchases_stripe_charge_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_charge_id" + ] + }, + "kiloclaw_earlybird_purchases_manual_payment_id_unique": { + "name": "kiloclaw_earlybird_purchases_manual_payment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "manual_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_email_log": { + "name": "kiloclaw_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "'epoch'" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_email_log_user_type_global": { + "name": "UQ_kiloclaw_email_log_user_type_global", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_email_log_user_instance_type_period": { + "name": "UQ_kiloclaw_email_log_user_instance_type_period", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_email_log_type_sent_instance": { + "name": "IDX_kiloclaw_email_log_type_sent_instance", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_email_log_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_google_oauth_connections": { + "name": "kiloclaw_google_oauth_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'google'" + }, + "account_email": { + "name": "account_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_subject": { + "name": "account_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_profile": { + "name": "credential_profile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kilo_owned'" + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "grants_by_source": { + "name": "grants_by_source", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_google_oauth_connections_instance": { + "name": "UQ_kiloclaw_google_oauth_connections_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_status": { + "name": "IDX_kiloclaw_google_oauth_connections_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_provider": { + "name": "IDX_kiloclaw_google_oauth_connections_provider", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_google_oauth_connections", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_google_oauth_connections_status_check": { + "name": "kiloclaw_google_oauth_connections_status_check", + "value": "\"kiloclaw_google_oauth_connections\".\"status\" IN ('active', 'action_required', 'disconnected')" + }, + "kiloclaw_google_oauth_connections_credential_profile_check": { + "name": "kiloclaw_google_oauth_connections_credential_profile_check", + "value": "\"kiloclaw_google_oauth_connections\".\"credential_profile\" IN ('legacy', 'kilo_owned')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_image_catalog": { + "name": "kiloclaw_image_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_digest": { + "name": "image_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rollout_percent": { + "name": "rollout_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_latest": { + "name": "is_latest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kiloclaw_image_catalog_status": { + "name": "IDX_kiloclaw_image_catalog_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_image_catalog_variant": { + "name": "IDX_kiloclaw_image_catalog_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_latest_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_latest_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_candidate_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_candidate_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = false AND \"kiloclaw_image_catalog\".\"rollout_percent\" > 0 AND \"kiloclaw_image_catalog\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_image_catalog_image_tag_unique": { + "name": "kiloclaw_image_catalog_image_tag_unique", + "nullsNotDistinct": false, + "columns": [ + "image_tag" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_aliases": { + "name": "kiloclaw_inbound_email_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_inbound_email_aliases_instance_id": { + "name": "IDX_kiloclaw_inbound_email_aliases_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_inbound_email_aliases_active_instance": { + "name": "UQ_kiloclaw_inbound_email_aliases_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_inbound_email_aliases\".\"retired_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_inbound_email_aliases", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_reserved_aliases": { + "name": "kiloclaw_inbound_email_reserved_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_instances": { + "name": "kiloclaw_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fly'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbound_email_enabled": { + "name": "inbound_email_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inactive_trial_stopped_at": { + "name": "inactive_trial_stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracked_image_tag": { + "name": "tracked_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instance_type": { + "name": "instance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_size_override": { + "name": "admin_size_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_instances_active": { + "name": "UQ_kiloclaw_instances_active", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_personal_by_user": { + "name": "IDX_kiloclaw_instances_active_personal_by_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_user_org": { + "name": "IDX_kiloclaw_instances_active_org_by_user_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_org_created": { + "name": "IDX_kiloclaw_instances_active_org_by_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_user_id_created_at": { + "name": "IDX_kiloclaw_instances_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_tracked_image_tag": { + "name": "IDX_kiloclaw_instances_tracked_image_tag", + "columns": [ + { + "expression": "tracked_image_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_instance_type": { + "name": "IDX_kiloclaw_instances_instance_type", + "columns": [ + { + "expression": "instance_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_admin_size_override": { + "name": "IDX_kiloclaw_instances_admin_size_override", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"admin_size_override\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_instances_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_instances_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_instances_organization_id_organizations_id_fk": { + "name": "kiloclaw_instances_organization_id_organizations_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_kiloclaw_instances_instance_type": { + "name": "CHK_kiloclaw_instances_instance_type", + "value": "\"kiloclaw_instances\".\"instance_type\" IS NULL OR \"kiloclaw_instances\".\"instance_type\" IN ('perf-1-3', 'perf-4-8', 'perf-4-16', 'shared-2-3', 'shared-2-4', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_morning_briefing_configs": { + "name": "kiloclaw_morning_briefing_configs", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0 7 * * *'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "interest_topics": { + "name": "interest_topics", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_morning_briefing_configs_enabled": { + "name": "IDX_kiloclaw_morning_briefing_configs_enabled", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_morning_briefing_configs\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_morning_briefing_configs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_notifications": { + "name": "kiloclaw_scheduled_action_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'notice'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel": { + "name": "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_notifications_pending": { + "name": "IDX_kiloclaw_scheduled_action_notifications_pending", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk": { + "name": "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk", + "tableFrom": "kiloclaw_scheduled_action_notifications", + "tableTo": "kiloclaw_scheduled_action_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_stages": { + "name": "kiloclaw_scheduled_action_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_index": { + "name": "stage_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "notice_sent_at": { + "name": "notice_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_stages_parent_index": { + "name": "UQ_kiloclaw_scheduled_action_stages_parent_index", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_stages_notice_due": { + "name": "IDX_kiloclaw_scheduled_action_stages_notice_due", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_stages\".\"notice_sent_at\" IS NULL AND \"kiloclaw_scheduled_action_stages\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_stages", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_targets": { + "name": "kiloclaw_scheduled_action_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_image_tag": { + "name": "source_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_targets_parent_instance": { + "name": "UQ_kiloclaw_scheduled_action_targets_parent_instance", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_stage": { + "name": "IDX_kiloclaw_scheduled_action_targets_stage", + "columns": [ + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_pending_by_instance": { + "name": "IDX_kiloclaw_scheduled_action_targets_pending_by_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_targets\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk": { + "name": "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_action_stages", + "columnsFrom": [ + "stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_actions": { + "name": "kiloclaw_scheduled_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_pins": { + "name": "override_pins", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notice_lead_hours": { + "name": "notice_lead_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "notice_subject": { + "name": "notice_subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "notice_body": { + "name": "notice_body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_count": { + "name": "total_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "IDX_kiloclaw_scheduled_actions_status": { + "name": "IDX_kiloclaw_scheduled_actions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_action_type": { + "name": "IDX_kiloclaw_scheduled_actions_action_type", + "columns": [ + { + "expression": "action_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_created_by": { + "name": "IDX_kiloclaw_scheduled_actions_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "target_image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_subscription_change_log": { + "name": "kiloclaw_subscription_change_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_subscription_change_log_subscription_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_subscription_created_at", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscription_change_log_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscription_change_log", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscription_change_log_actor_type_check": { + "name": "kiloclaw_subscription_change_log_actor_type_check", + "value": "\"kiloclaw_subscription_change_log\".\"actor_type\" IN ('user', 'system')" + }, + "kiloclaw_subscription_change_log_action_check": { + "name": "kiloclaw_subscription_change_log_action_check", + "value": "\"kiloclaw_subscription_change_log\".\"action\" IN ('created', 'status_changed', 'plan_switched', 'period_advanced', 'canceled', 'reactivated', 'suspended', 'destruction_scheduled', 'reassigned', 'backfilled', 'payment_source_changed', 'schedule_changed', 'admin_override')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_subscriptions": { + "name": "kiloclaw_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transferred_to_subscription_id": { + "name": "transferred_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "access_origin": { + "name": "access_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_source": { + "name": "payment_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kiloclaw_price_version": { + "name": "kiloclaw_price_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_plan": { + "name": "scheduled_plan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_by": { + "name": "scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pending_conversion": { + "name": "pending_conversion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trial_started_at": { + "name": "trial_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "commit_ends_at": { + "name": "commit_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "destruction_deadline": { + "name": "destruction_deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_requested_at": { + "name": "auto_resume_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_retry_after": { + "name": "auto_resume_retry_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_attempt_count": { + "name": "auto_resume_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_top_up_triggered_for_period": { + "name": "auto_top_up_triggered_for_period", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_subscriptions_status": { + "name": "IDX_kiloclaw_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_id": { + "name": "IDX_kiloclaw_subscriptions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_status": { + "name": "IDX_kiloclaw_subscriptions_user_status", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_price_version": { + "name": "IDX_kiloclaw_subscriptions_price_version", + "columns": [ + { + "expression": "kiloclaw_price_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_transferred_to": { + "name": "IDX_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_stripe_schedule_id": { + "name": "IDX_kiloclaw_subscriptions_stripe_schedule_id", + "columns": [ + { + "expression": "stripe_schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_auto_resume_retry_after": { + "name": "IDX_kiloclaw_subscriptions_auto_resume_retry_after", + "columns": [ + { + "expression": "auto_resume_retry_after", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_instance": { + "name": "UQ_kiloclaw_subscriptions_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_transferred_to": { + "name": "UQ_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"transferred_to_subscription_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_earlybird_origin": { + "name": "IDX_kiloclaw_subscriptions_earlybird_origin", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "access_origin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_subscriptions\".\"access_origin\" = 'earlybird'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscriptions_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "transferred_to_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_subscriptions_stripe_subscription_id_unique": { + "name": "kiloclaw_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscriptions_price_version_check": { + "name": "kiloclaw_subscriptions_price_version_check", + "value": "\"kiloclaw_subscriptions\".\"kiloclaw_price_version\" IN ('2026-03-19', '2026-05-10')" + }, + "kiloclaw_subscriptions_plan_check": { + "name": "kiloclaw_subscriptions_plan_check", + "value": "\"kiloclaw_subscriptions\".\"plan\" IN ('trial', 'commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_plan_check": { + "name": "kiloclaw_subscriptions_scheduled_plan_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_plan\" IN ('commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_by_check": { + "name": "kiloclaw_subscriptions_scheduled_by_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_by\" IN ('auto', 'user')" + }, + "kiloclaw_subscriptions_status_check": { + "name": "kiloclaw_subscriptions_status_check", + "value": "\"kiloclaw_subscriptions\".\"status\" IN ('trialing', 'active', 'past_due', 'canceled', 'unpaid')" + }, + "kiloclaw_subscriptions_access_origin_check": { + "name": "kiloclaw_subscriptions_access_origin_check", + "value": "\"kiloclaw_subscriptions\".\"access_origin\" IN ('earlybird')" + }, + "kiloclaw_subscriptions_payment_source_check": { + "name": "kiloclaw_subscriptions_payment_source_check", + "value": "\"kiloclaw_subscriptions\".\"payment_source\" IN ('stripe', 'credits')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_terminal_renewal_failures": { + "name": "kiloclaw_terminal_renewal_failures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "renewal_boundary": { + "name": "renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unresolved'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_failure_at": { + "name": "first_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_failure_message": { + "name": "last_failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_type": { + "name": "resolution_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_id": { + "name": "resolution_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_at": { + "name": "resolution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary": { + "name": "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_unresolved": { + "name": "IDX_kiloclaw_terminal_renewal_failures_unresolved", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_terminal_renewal_failures\".\"status\" = 'unresolved'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at": { + "name": "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_terminal_renewal_failures", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_terminal_renewal_failures_status_check": { + "name": "kiloclaw_terminal_renewal_failures_status_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"status\" IN ('unresolved', 'resolved', 'waived', 'superseded')" + }, + "kiloclaw_terminal_renewal_failures_last_failure_code_check": { + "name": "kiloclaw_terminal_renewal_failures_last_failure_code_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"last_failure_code\" IN ('credit_balance_read_failed', 'renewal_transaction_failed', 'auto_top_up_marker_write_failed', 'worker_timeout', 'poison_payload', 'queue_delivery_exhausted')" + }, + "kiloclaw_terminal_renewal_failures_resolution_actor_type_check": { + "name": "kiloclaw_terminal_renewal_failures_resolution_actor_type_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"resolution_actor_type\" IN ('operator', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_version_pins": { + "name": "kiloclaw_version_pins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_by": { + "name": "pinned_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk": { + "name": "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kilocode_users", + "columnsFrom": [ + "pinned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_version_pins_instance_id_unique": { + "name": "kiloclaw_version_pins_instance_id_unique", + "nullsNotDistinct": false, + "columns": [ + "instance_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilocode_users": { + "name": "kilocode_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "google_user_email": { + "name": "google_user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_name": { + "name": "google_user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_image_url": { + "name": "google_user_image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "kilo_pass_threshold": { + "name": "kilo_pass_threshold", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_store_account_token": { + "name": "app_store_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_manage_credits": { + "name": "can_manage_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "has_validation_stytch": { + "name": "has_validation_stytch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_validation_novel_card_with_hold": { + "name": "has_validation_novel_card_with_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_at": { + "name": "blocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_by_kilo_user_id": { + "name": "blocked_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_token_pepper": { + "name": "api_token_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_session_pepper": { + "name": "web_session_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "kiloclaw_early_access": { + "name": "kiloclaw_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cohorts": { + "name": "cohorts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_welcome_form": { + "name": "completed_welcome_form", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "linkedin_url": { + "name": "linkedin_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discord_server_membership_verified_at": { + "name": "discord_server_membership_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "openrouter_upstream_safety_identifier": { + "name": "openrouter_upstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openrouter_downstream_safety_identifier": { + "name": "openrouter_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_downstream_safety_identifier": { + "name": "vercel_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_source": { + "name": "customer_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signup_ip": { + "name": "signup_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_deletion_requested_at": { + "name": "account_deletion_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_domain": { + "name": "email_domain", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kilocode_users_signup_ip_created_at": { + "name": "IDX_kilocode_users_signup_ip_created_at", + "columns": [ + { + "expression": "signup_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_at": { + "name": "IDX_kilocode_users_blocked_at", + "columns": [ + { + "expression": "blocked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_by_kilo_user_id": { + "name": "IDX_kilocode_users_blocked_by_kilo_user_id", + "columns": [ + { + "expression": "blocked_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_upstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_upstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_upstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_upstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_downstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_downstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_downstream_safety_identifier\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_vercel_downstream_safety_identifier": { + "name": "UQ_kilocode_users_vercel_downstream_safety_identifier", + "columns": [ + { + "expression": "vercel_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"vercel_downstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_normalized_email": { + "name": "IDX_kilocode_users_normalized_email", + "columns": [ + { + "expression": "normalized_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_email_domain": { + "name": "IDX_kilocode_users_email_domain", + "columns": [ + { + "expression": "email_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilocode_users_app_store_account_token_unique": { + "name": "kilocode_users_app_store_account_token_unique", + "nullsNotDistinct": false, + "columns": [ + "app_store_account_token" + ] + }, + "UQ_b1afacbcf43f2c7c4cb9f7e7faa": { + "name": "UQ_b1afacbcf43f2c7c4cb9f7e7faa", + "nullsNotDistinct": false, + "columns": [ + "google_user_email" + ] + } + }, + "policies": {}, + "checkConstraints": { + "blocked_reason_not_empty": { + "name": "blocked_reason_not_empty", + "value": "length(blocked_reason) > 0" + }, + "kilocode_users_can_manage_credits_requires_admin_check": { + "name": "kilocode_users_can_manage_credits_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_manage_credits\" OR \"kilocode_users\".\"is_admin\"" + } + }, + "isRLSEnabled": false + }, + "public.magic_link_tokens": { + "name": "magic_link_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_magic_link_tokens_email": { + "name": "idx_magic_link_tokens_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_magic_link_tokens_expires_at": { + "name": "idx_magic_link_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_expires_at_future": { + "name": "check_expires_at_future", + "value": "\"magic_link_tokens\".\"expires_at\" > \"magic_link_tokens\".\"created_at\"" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_assignments": { + "name": "mcp_gateway_assignments", + "schema": "", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_kilo_user_id": { + "name": "assigned_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "single_user_slot": { + "name": "single_user_slot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_assignments_active": { + "name": "UQ_mcp_gateway_assignments_active", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_assignments\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_assignments_single_user_slot": { + "name": "UQ_mcp_gateway_assignments_single_user_slot", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "single_user_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_assignments\".\"revoked_at\" is null and \"mcp_gateway_assignments\".\"single_user_slot\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_assignments_config": { + "name": "IDX_mcp_gateway_assignments_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_assignments_user": { + "name": "IDX_mcp_gateway_assignments_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_assignments_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_assignments_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_assignments_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_assignments_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_assignments_assigned_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_assignments_assigned_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_gateway_audit_events": { + "name": "mcp_gateway_audit_events", + "schema": "", + "columns": { + "audit_event_id": { + "name": "audit_event_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_metadata": { + "name": "correlation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_audit_events_config": { + "name": "IDX_mcp_gateway_audit_events_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_owner": { + "name": "IDX_mcp_gateway_audit_events_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_created_at": { + "name": "IDX_mcp_gateway_audit_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_audit_events_owner_scope": { + "name": "mcp_gateway_audit_events_owner_scope", + "value": "\"mcp_gateway_audit_events\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_audit_events_outcome": { + "name": "mcp_gateway_audit_events_outcome", + "value": "\"mcp_gateway_audit_events\".\"outcome\" IN ('success', 'failure', 'blocked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_codes": { + "name": "mcp_gateway_authorization_codes", + "schema": "", + "columns": { + "authorization_code_id": { + "name": "authorization_code_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_codes_code_hash": { + "name": "UQ_mcp_gateway_authorization_codes_code_hash", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_expires_at": { + "name": "IDX_mcp_gateway_authorization_codes_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_client": { + "name": "IDX_mcp_gateway_authorization_codes_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_codes_owner_scope": { + "name": "mcp_gateway_authorization_codes_owner_scope", + "value": "\"mcp_gateway_authorization_codes\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_requests": { + "name": "mcp_gateway_authorization_requests", + "schema": "", + "columns": { + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_state_hash": { + "name": "request_state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_scopes": { + "name": "requested_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "oauth_state": { + "name": "oauth_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_status": { + "name": "request_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_requests_state_hash": { + "name": "UQ_mcp_gateway_authorization_requests_state_hash", + "columns": [ + { + "expression": "request_state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_config": { + "name": "IDX_mcp_gateway_authorization_requests_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_user": { + "name": "IDX_mcp_gateway_authorization_requests_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_expires_at": { + "name": "IDX_mcp_gateway_authorization_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_requests_owner_scope": { + "name": "mcp_gateway_authorization_requests_owner_scope", + "value": "\"mcp_gateway_authorization_requests\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_authorization_requests_status": { + "name": "mcp_gateway_authorization_requests_status", + "value": "\"mcp_gateway_authorization_requests\".\"request_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_config_secrets": { + "name": "mcp_gateway_config_secrets", + "schema": "", + "columns": { + "config_secret_id": { + "name": "config_secret_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_kind": { + "name": "secret_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_config_secrets_active_kind": { + "name": "UQ_mcp_gateway_config_secrets_active_kind", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_config_secrets\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_config_secrets_config": { + "name": "IDX_mcp_gateway_config_secrets_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_config_secrets", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_config_secrets_version_positive": { + "name": "mcp_gateway_config_secrets_version_positive", + "value": "\"mcp_gateway_config_secrets\".\"secret_version\" > 0" + }, + "mcp_gateway_config_secrets_kind": { + "name": "mcp_gateway_config_secrets_kind", + "value": "\"mcp_gateway_config_secrets\".\"secret_kind\" IN ('static_provider_credentials', 'dynamic_registration', 'static_headers')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_configs": { + "name": "mcp_gateway_configs", + "schema": "", + "columns": { + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharing_mode": { + "name": "sharing_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_scope_source": { + "name": "provider_scope_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "provider_resource": { + "name": "provider_resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "path_passthrough": { + "name": "path_passthrough", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "discovered_provider_metadata": { + "name": "discovered_provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "registry_metadata": { + "name": "registry_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "auxiliary_headers": { + "name": "auxiliary_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_configs_owner": { + "name": "IDX_mcp_gateway_configs_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_enabled": { + "name": "IDX_mcp_gateway_configs_enabled", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_remote_url": { + "name": "IDX_mcp_gateway_configs_remote_url", + "columns": [ + { + "expression": "remote_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_configs_name_not_empty": { + "name": "mcp_gateway_configs_name_not_empty", + "value": "length(trim(\"mcp_gateway_configs\".\"name\")) > 0" + }, + "mcp_gateway_configs_config_version_positive": { + "name": "mcp_gateway_configs_config_version_positive", + "value": "\"mcp_gateway_configs\".\"config_version\" > 0" + }, + "mcp_gateway_configs_personal_single_user": { + "name": "mcp_gateway_configs_personal_single_user", + "value": "\"mcp_gateway_configs\".\"owner_scope\" <> 'personal' OR \"mcp_gateway_configs\".\"sharing_mode\" = 'single_user'" + }, + "mcp_gateway_configs_owner_scope": { + "name": "mcp_gateway_configs_owner_scope", + "value": "\"mcp_gateway_configs\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_configs_auth_mode": { + "name": "mcp_gateway_configs_auth_mode", + "value": "\"mcp_gateway_configs\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_configs_sharing_mode": { + "name": "mcp_gateway_configs_sharing_mode", + "value": "\"mcp_gateway_configs\".\"sharing_mode\" IN ('single_user', 'multi_user')" + }, + "mcp_gateway_configs_provider_scope_source": { + "name": "mcp_gateway_configs_provider_scope_source", + "value": "\"mcp_gateway_configs\".\"provider_scope_source\" IN ('none', 'discovered', 'override')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connect_resources": { + "name": "mcp_gateway_connect_resources", + "schema": "", + "columns": { + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_status": { + "name": "route_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "route_version": { + "name": "route_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connect_resources_route_key": { + "name": "UQ_mcp_gateway_connect_resources_route_key", + "columns": [ + { + "expression": "route_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_connect_resources_active_config": { + "name": "UQ_mcp_gateway_connect_resources_active_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connect_resources\".\"route_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_config": { + "name": "IDX_mcp_gateway_connect_resources_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_canonical_url": { + "name": "IDX_mcp_gateway_connect_resources_canonical_url", + "columns": [ + { + "expression": "canonical_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connect_resources", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connect_resources_route_key_format": { + "name": "mcp_gateway_connect_resources_route_key_format", + "value": "\"mcp_gateway_connect_resources\".\"route_key\" ~ '^[A-Za-z0-9_-]{32,}$'" + }, + "mcp_gateway_connect_resources_route_version_positive": { + "name": "mcp_gateway_connect_resources_route_version_positive", + "value": "\"mcp_gateway_connect_resources\".\"route_version\" > 0" + }, + "mcp_gateway_connect_resources_owner_scope": { + "name": "mcp_gateway_connect_resources_owner_scope", + "value": "\"mcp_gateway_connect_resources\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connect_resources_route_status": { + "name": "mcp_gateway_connect_resources_route_status", + "value": "\"mcp_gateway_connect_resources\".\"route_status\" IN ('active', 'rotated', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connection_instances": { + "name": "mcp_gateway_connection_instances", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_status": { + "name": "instance_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "instance_version": { + "name": "instance_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connection_instances_non_terminal": { + "name": "UQ_mcp_gateway_connection_instances_non_terminal", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_config": { + "name": "IDX_mcp_gateway_connection_instances_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_user": { + "name": "IDX_mcp_gateway_connection_instances_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connection_instances_version_positive": { + "name": "mcp_gateway_connection_instances_version_positive", + "value": "\"mcp_gateway_connection_instances\".\"instance_version\" > 0" + }, + "mcp_gateway_connection_instances_owner_scope": { + "name": "mcp_gateway_connection_instances_owner_scope", + "value": "\"mcp_gateway_connection_instances\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connection_instances_status": { + "name": "mcp_gateway_connection_instances_status", + "value": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth', 'revoked', 'removed')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_clients": { + "name": "mcp_gateway_oauth_clients", + "schema": "", + "columns": { + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_token_hash": { + "name": "registration_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "declared_scopes": { + "name": "declared_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "registration_access_token_expires_at": { + "name": "registration_access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_clients_client_id": { + "name": "UQ_mcp_gateway_oauth_clients_client_id", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_oauth_clients_registration_token_hash": { + "name": "UQ_mcp_gateway_oauth_clients_registration_token_hash", + "columns": [ + { + "expression": "registration_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_clients_deleted_at": { + "name": "IDX_mcp_gateway_oauth_clients_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_clients_client_id_format": { + "name": "mcp_gateway_oauth_clients_client_id_format", + "value": "\"mcp_gateway_oauth_clients\".\"client_id\" ~ '^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+$'" + }, + "mcp_gateway_oauth_clients_auth_method": { + "name": "mcp_gateway_oauth_clients_auth_method", + "value": "\"mcp_gateway_oauth_clients\".\"token_endpoint_auth_method\" IN ('none', 'client_secret_post', 'client_secret_basic')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_pending_provider_authorizations": { + "name": "mcp_gateway_pending_provider_authorizations", + "schema": "", + "columns": { + "pending_provider_authorization_id": { + "name": "pending_provider_authorization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_authorization_endpoint": { + "name": "provider_authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_token_endpoint": { + "name": "provider_token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pending_status": { + "name": "pending_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_pending_provider_authorizations_state_hash": { + "name": "UQ_mcp_gateway_pending_provider_authorizations_state_hash", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_config": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_expires_at": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_pending_provider_authorizations_config_version_positive": { + "name": "mcp_gateway_pending_provider_authorizations_config_version_positive", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"config_version\" > 0" + }, + "mcp_gateway_pending_provider_authorizations_owner_scope": { + "name": "mcp_gateway_pending_provider_authorizations_owner_scope", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_pending_provider_authorizations_auth_mode": { + "name": "mcp_gateway_pending_provider_authorizations_auth_mode", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_pending_provider_authorizations_status": { + "name": "mcp_gateway_pending_provider_authorizations_status", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"pending_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_provider_grants": { + "name": "mcp_gateway_provider_grants", + "schema": "", + "columns": { + "provider_grant_id": { + "name": "provider_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "encrypted_grant": { + "name": "encrypted_grant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject": { + "name": "provider_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_scope": { + "name": "grant_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_provider_grants_active_instance": { + "name": "UQ_mcp_gateway_provider_grants_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_provider_grants\".\"grant_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_provider_grants_instance": { + "name": "IDX_mcp_gateway_provider_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_provider_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_provider_grants_version_positive": { + "name": "mcp_gateway_provider_grants_version_positive", + "value": "\"mcp_gateway_provider_grants\".\"grant_version\" > 0" + }, + "mcp_gateway_provider_grants_status": { + "name": "mcp_gateway_provider_grants_status", + "value": "\"mcp_gateway_provider_grants\".\"grant_status\" IN ('active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_rate_limit_windows": { + "name": "mcp_gateway_rate_limit_windows", + "schema": "", + "columns": { + "rate_limit_window_id": { + "name": "rate_limit_window_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_started_at": { + "name": "window_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_rate_limit_windows_ip_window": { + "name": "UQ_mcp_gateway_rate_limit_windows_ip_window", + "columns": [ + { + "expression": "ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_rate_limit_windows_window": { + "name": "IDX_mcp_gateway_rate_limit_windows_window", + "columns": [ + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_rate_limit_windows_attempt_count_non_negative": { + "name": "mcp_gateway_rate_limit_windows_attempt_count_non_negative", + "value": "\"mcp_gateway_rate_limit_windows\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_refresh_tokens": { + "name": "mcp_gateway_refresh_tokens", + "schema": "", + "columns": { + "refresh_token_id": { + "name": "refresh_token_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotated_from_refresh_token_id": { + "name": "rotated_from_refresh_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_refresh_tokens_token_hash": { + "name": "UQ_mcp_gateway_refresh_tokens_token_hash", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_user": { + "name": "IDX_mcp_gateway_refresh_tokens_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_config": { + "name": "IDX_mcp_gateway_refresh_tokens_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_consumed_at": { + "name": "IDX_mcp_gateway_refresh_tokens_consumed_at", + "columns": [ + { + "expression": "consumed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_refresh_tokens_owner_scope": { + "name": "mcp_gateway_refresh_tokens_owner_scope", + "value": "\"mcp_gateway_refresh_tokens\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage": { + "name": "microdollar_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_abuse_classification": { + "name": "idx_abuse_classification", + "columns": [ + { + "expression": "abuse_classification", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id_created_at2": { + "name": "idx_kilo_user_id_created_at2", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_organization_id": { + "name": "idx_microdollar_usage_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily": { + "name": "microdollar_usage_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_microdollar_usage_daily_personal": { + "name": "idx_microdollar_usage_daily_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_daily_org": { + "name": "idx_microdollar_usage_daily_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_metadata": { + "name": "microdollar_usage_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_ip_id": { + "name": "http_ip_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_latitude": { + "name": "vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_longitude": { + "name": "vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason_id": { + "name": "finish_reason_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name_id": { + "name": "editor_name_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_kind_id": { + "name": "api_kind_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode_id": { + "name": "mode_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_model_id": { + "name": "auto_model_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_microdollar_usage_metadata_created_at": { + "name": "idx_microdollar_usage_metadata_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_metadata_session_id": { + "name": "idx_microdollar_usage_metadata_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage_metadata\".\"session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk": { + "name": "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_user_agent", + "columnsFrom": [ + "http_user_agent_id" + ], + "columnsTo": [ + "http_user_agent_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk": { + "name": "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_ip", + "columnsFrom": [ + "http_ip_id" + ], + "columnsTo": [ + "http_ip_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_city", + "columnsFrom": [ + "vercel_ip_city_id" + ], + "columnsTo": [ + "vercel_ip_city_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_country", + "columnsFrom": [ + "vercel_ip_country_id" + ], + "columnsTo": [ + "vercel_ip_country_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk": { + "name": "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "ja4_digest", + "columnsFrom": [ + "ja4_digest_id" + ], + "columnsTo": [ + "ja4_digest_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk": { + "name": "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "system_prompt_prefix", + "columnsFrom": [ + "system_prompt_prefix_id" + ], + "columnsTo": [ + "system_prompt_prefix_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mode": { + "name": "mode", + "schema": "", + "columns": { + "mode_id": { + "name": "mode_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_mode": { + "name": "UQ_mode", + "columns": [ + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_stats": { + "name": "model_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_stealth": { + "name": "is_stealth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_recommended": { + "name": "is_recommended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "openrouter_id": { + "name": "openrouter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aa_slug": { + "name": "aa_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_creator": { + "name": "model_creator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_slug": { + "name": "creator_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_date": { + "name": "release_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "price_input": { + "name": "price_input", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "price_output": { + "name": "price_output", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "coding_index": { + "name": "coding_index", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "speed_tokens_per_sec": { + "name": "speed_tokens_per_sec", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "openrouter_data": { + "name": "openrouter_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "benchmarks": { + "name": "benchmarks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chart_data": { + "name": "chart_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_stats_openrouter_id": { + "name": "IDX_model_stats_openrouter_id", + "columns": [ + { + "expression": "openrouter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_slug": { + "name": "IDX_model_stats_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_is_active": { + "name": "IDX_model_stats_is_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_creator_slug": { + "name": "IDX_model_stats_creator_slug", + "columns": [ + { + "expression": "creator_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_price_input": { + "name": "IDX_model_stats_price_input", + "columns": [ + { + "expression": "price_input", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_coding_index": { + "name": "IDX_model_stats_coding_index", + "columns": [ + { + "expression": "coding_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_context_length": { + "name": "IDX_model_stats_context_length", + "columns": [ + { + "expression": "context_length", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_stats_openrouter_id_unique": { + "name": "model_stats_openrouter_id_unique", + "nullsNotDistinct": false, + "columns": [ + "openrouter_id" + ] + }, + "model_stats_slug_unique": { + "name": "model_stats_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_eval_ingestions": { + "name": "model_eval_ingestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bench_eval_name": { + "name": "bench_eval_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bench_eval_url": { + "name": "bench_eval_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_stats_id": { + "name": "model_stats_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "n_total_trials": { + "name": "n_total_trials", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "n_attempts": { + "name": "n_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_score": { + "name": "total_score", + "type": "numeric(14, 6)", + "primaryKey": false, + "notNull": true + }, + "overall_score": { + "name": "overall_score", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": true + }, + "n_errored": { + "name": "n_errored", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "avg_cost_microdollars": { + "name": "avg_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_input_tokens": { + "name": "avg_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_output_tokens": { + "name": "avg_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_cache_read_tokens": { + "name": "avg_cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cache_read_tokens": { + "name": "total_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_execution_ms": { + "name": "avg_execution_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promoted_at": { + "name": "promoted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "promoted_by_email": { + "name": "promoted_by_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "promotion_note": { + "name": "promotion_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_eval_ingestions_lookup": { + "name": "IDX_model_eval_ingestions_lookup", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "promoted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_model_stats": { + "name": "IDX_model_eval_ingestions_model_stats", + "columns": [ + { + "expression": "model_stats_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_promoted_by_email_lower": { + "name": "IDX_model_eval_ingestions_promoted_by_email_lower", + "columns": [ + { + "expression": "LOWER(\"promoted_by_email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_eval_ingestions_model_stats_id_model_stats_id_fk": { + "name": "model_eval_ingestions_model_stats_id_model_stats_id_fk", + "tableFrom": "model_eval_ingestions", + "tableTo": "model_stats", + "columnsFrom": [ + "model_stats_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_eval_ingestions_bench_eval_name_unique": { + "name": "model_eval_ingestions_bench_eval_name_unique", + "nullsNotDistinct": false, + "columns": [ + "bench_eval_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_experiment": { + "name": "model_experiment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "public_model_id": { + "name": "public_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_model_experiment_public_model_id_routing": { + "name": "UQ_model_experiment_public_model_id_routing", + "columns": [ + { + "expression": "public_model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"model_experiment\".\"status\" IN ('active', 'paused')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_experiment_status": { + "name": "IDX_model_experiment_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_created_by_user_id_kilocode_users_id_fk": { + "name": "model_experiment_created_by_user_id_kilocode_users_id_fk", + "tableFrom": "model_experiment", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_experiment_status_valid": { + "name": "model_experiment_status_valid", + "value": "\"model_experiment\".\"status\" IN ('draft', 'active', 'paused', 'completed')" + }, + "model_experiment_active_not_archived": { + "name": "model_experiment_active_not_archived", + "value": "\"model_experiment\".\"status\" <> 'active' OR \"model_experiment\".\"is_archived\" = false" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_request": { + "name": "model_experiment_request", + "schema": "", + "columns": { + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "variant_version_id": { + "name": "variant_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_subject": { + "name": "allocation_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_kind": { + "name": "request_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_body_sha256": { + "name": "request_body_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "was_truncated": { + "name": "was_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_request_variant_version_created_at": { + "name": "IDX_model_experiment_request_variant_version_created_at", + "columns": [ + { + "expression": "variant_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_experiment_request_client_request_id": { + "name": "IDX_model_experiment_request_client_request_id", + "columns": [ + { + "expression": "client_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"model_experiment_request\".\"client_request_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_request_usage_id_microdollar_usage_id_fk": { + "name": "model_experiment_request_usage_id_microdollar_usage_id_fk", + "tableFrom": "model_experiment_request", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_experiment_request_variant_version_id_model_experiment_variant_version_id_fk": { + "name": "model_experiment_request_variant_version_id_model_experiment_variant_version_id_fk", + "tableFrom": "model_experiment_request", + "tableTo": "model_experiment_variant_version", + "columnsFrom": [ + "variant_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "model_experiment_request_usage_id_created_at_pk": { + "name": "model_experiment_request_usage_id_created_at_pk", + "columns": [ + "usage_id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_experiment_request_allocation_subject_valid": { + "name": "model_experiment_request_allocation_subject_valid", + "value": "\"model_experiment_request\".\"allocation_subject\" IN ('user', 'machine', 'ip')" + }, + "model_experiment_request_request_kind_valid": { + "name": "model_experiment_request_request_kind_valid", + "value": "\"model_experiment_request\".\"request_kind\" IN ('chat_completions', 'messages', 'responses')" + }, + "model_experiment_request_request_body_sha256_format": { + "name": "model_experiment_request_request_body_sha256_format", + "value": "\"model_experiment_request\".\"request_body_sha256\" ~ '^[0-9a-f]{64}$' OR \"model_experiment_request\".\"request_body_sha256\" IN ('__failed__', '__deleted__')" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_variant": { + "name": "model_experiment_variant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "experiment_id": { + "name": "experiment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_variant_experiment_id": { + "name": "IDX_model_experiment_variant_experiment_id", + "columns": [ + { + "expression": "experiment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_variant_experiment_id_model_experiment_id_fk": { + "name": "model_experiment_variant_experiment_id_model_experiment_id_fk", + "tableFrom": "model_experiment_variant", + "tableTo": "model_experiment", + "columnsFrom": [ + "experiment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_model_experiment_variant_experiment_label": { + "name": "UQ_model_experiment_variant_experiment_label", + "nullsNotDistinct": false, + "columns": [ + "experiment_id", + "label" + ] + } + }, + "policies": {}, + "checkConstraints": { + "model_experiment_variant_weight_positive": { + "name": "model_experiment_variant_weight_positive", + "value": "\"model_experiment_variant\".\"weight\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_variant_version": { + "name": "model_experiment_variant_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "variant_id": { + "name": "variant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_variant_version_variant_effective": { + "name": "IDX_model_experiment_variant_version_variant_effective", + "columns": [ + { + "expression": "variant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_variant_version_variant_id_model_experiment_variant_id_fk": { + "name": "model_experiment_variant_version_variant_id_model_experiment_variant_id_fk", + "tableFrom": "model_experiment_variant_version", + "tableTo": "model_experiment_variant", + "columnsFrom": [ + "variant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_experiment_variant_version_created_by_kilocode_users_id_fk": { + "name": "model_experiment_variant_version_created_by_kilocode_users_id_fk", + "tableFrom": "model_experiment_variant_version", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models_by_provider": { + "name": "models_by_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "openrouter": { + "name": "openrouter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "vercel": { + "name": "vercel", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_audit_logs": { + "name": "organization_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_audit_logs_organization_id": { + "name": "IDX_organization_audit_logs_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_action": { + "name": "IDX_organization_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_actor_id": { + "name": "IDX_organization_audit_logs_actor_id", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_created_at": { + "name": "IDX_organization_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "authentication_requirement": { + "name": "authentication_requirement", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "sso_source_organization_id": { + "name": "sso_source_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_invitations_token": { + "name": "UQ_organization_invitations_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_org_id": { + "name": "IDX_organization_invitations_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_email": { + "name": "IDX_organization_invitations_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_expires_at": { + "name": "IDX_organization_invitations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_sso_source_organization_id_organizations_id_fk": { + "name": "organization_invitations_sso_source_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "sso_source_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_membership_removals": { + "name": "organization_membership_removals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_by": { + "name": "removed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_role": { + "name": "previous_role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_org_membership_removals_org_id": { + "name": "IDX_org_membership_removals_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_org_membership_removals_user_id": { + "name": "IDX_org_membership_removals_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_membership_removals_org_user": { + "name": "UQ_org_membership_removals_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_memberships": { + "name": "organization_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_memberships_org_id": { + "name": "IDX_organization_memberships_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_memberships_user_id": { + "name": "IDX_organization_memberships_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_memberships_org_user": { + "name": "UQ_organization_memberships_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_recommendation_dismissals": { + "name": "organization_recommendation_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recommendation_key": { + "name": "recommendation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_by_user_id": { + "name": "dismissed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk": { + "name": "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk": { + "name": "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "dismissed_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_recommendation_dismissals_org_key": { + "name": "UQ_org_recommendation_dismissals_org_key", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "recommendation_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_seats_purchases": { + "name": "organization_seats_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subscription_stripe_id": { + "name": "subscription_stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "subscription_status": { + "name": "subscription_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_cycle": { + "name": "billing_cycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + } + }, + "indexes": { + "IDX_organization_seats_org_id": { + "name": "IDX_organization_seats_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_expires_at": { + "name": "IDX_organization_seats_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_created_at": { + "name": "IDX_organization_seats_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_updated_at": { + "name": "IDX_organization_seats_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_starts_at": { + "name": "IDX_organization_seats_starts_at", + "columns": [ + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_seats_idempotency_key": { + "name": "UQ_organization_seats_idempotency_key", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_limits": { + "name": "organization_user_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_limit": { + "name": "microdollar_limit", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_user_limits_org_id": { + "name": "IDX_organization_user_limits_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_limits_user_id": { + "name": "IDX_organization_user_limits_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_limits_org_user": { + "name": "UQ_organization_user_limits_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_usage": { + "name": "organization_user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_usage": { + "name": "microdollar_usage", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_user_daily_usage_org_id": { + "name": "IDX_organization_user_daily_usage_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_daily_usage_user_id": { + "name": "IDX_organization_user_daily_usage_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_daily_usage_org_user_date": { + "name": "UQ_organization_user_daily_usage_org_user_date", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type", + "usage_date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "microdollars_balance": { + "name": "microdollars_balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_seats": { + "name": "require_seats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sso_domain": { + "name": "sso_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'teams'" + }, + "free_trial_end_at": { + "name": "free_trial_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "company_domain": { + "name": "company_domain", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_organizations_sso_domain": { + "name": "IDX_organizations_sso_domain", + "columns": [ + { + "expression": "sso_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organizations_parent_organization_id": { + "name": "IDX_organizations_parent_organization_id", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_parent_organization_id_organizations_id_fk": { + "name": "organizations_parent_organization_id_organizations_id_fk", + "tableFrom": "organizations", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organizations_name_not_empty_check": { + "name": "organizations_name_not_empty_check", + "value": "length(trim(\"organizations\".\"name\")) > 0" + }, + "organizations_not_parented_by_self_check": { + "name": "organizations_not_parented_by_self_check", + "value": "\"organizations\".\"parent_organization_id\" IS NULL OR \"organizations\".\"parent_organization_id\" <> \"organizations\".\"id\"" + } + }, + "isRLSEnabled": false + }, + "public.organization_modes": { + "name": "organization_modes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_organization_modes_organization_id": { + "name": "IDX_organization_modes_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_modes_org_id_slug": { + "name": "UQ_organization_modes_org_id_slug", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_methods": { + "name": "payment_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last4": { + "name": "last4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1": { + "name": "address_line1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line2": { + "name": "address_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_state": { + "name": "address_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "three_d_secure_supported": { + "name": "three_d_secure_supported", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regulated_status": { + "name": "regulated_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1_check_status": { + "name": "address_line1_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code_check_status": { + "name": "postal_code_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eligible_for_free_credits": { + "name": "eligible_for_free_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_data": { + "name": "stripe_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_d7d7fb15569674aaadcfbc0428": { + "name": "IDX_d7d7fb15569674aaadcfbc0428", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_e1feb919d0ab8a36381d5d5138": { + "name": "IDX_e1feb919d0ab8a36381d5d5138", + "columns": [ + { + "expression": "stripe_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_payment_methods_organization_id": { + "name": "IDX_payment_methods_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_29df1b0403df5792c96bbbfdbe6": { + "name": "UQ_29df1b0403df5792c96bbbfdbe6", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_impact_sale_reversals": { + "name": "pending_impact_sale_reversals", + "schema": "", + "columns": { + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "dispute_id": { + "name": "dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_impact_sale_reversals_attempt_count_non_negative_check": { + "name": "pending_impact_sale_reversals_attempt_count_non_negative_check", + "value": "\"pending_impact_sale_reversals\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.platform_access_token_credentials": { + "name": "platform_access_token_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_encrypted": { + "name": "token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_credential_type": { + "name": "provider_credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "provider_verified_at": { + "name": "provider_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_validated_at": { + "name": "last_validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "FK_platform_access_token_credentials_parent": { + "name": "FK_platform_access_token_credentials_parent", + "tableFrom": "platform_access_token_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_platform_access_token_credentials_platform_integration_id": { + "name": "UQ_platform_access_token_credentials_platform_integration_id", + "nullsNotDistinct": false, + "columns": [ + "platform_integration_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_integrations": { + "name": "platform_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_installation_id": { + "name": "platform_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_id": { + "name": "platform_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_login": { + "name": "platform_account_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "repository_access": { + "name": "repository_access", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repositories": { + "name": "repositories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repositories_synced_at": { + "name": "repositories_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_at": { + "name": "auth_invalid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_reason": { + "name": "auth_invalid_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "kilo_requester_user_id": { + "name": "kilo_requester_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_requester_account_id": { + "name": "platform_requester_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_status": { + "name": "integration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_by": { + "name": "suspended_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_integrations_owned_by_org_platform_inst": { + "name": "UQ_platform_integrations_owned_by_org_platform_inst", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_owned_by_user_platform_inst": { + "name": "UQ_platform_integrations_owned_by_user_platform_inst", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_slack_platform_inst": { + "name": "UQ_platform_integrations_slack_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'slack' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_linear_platform_inst": { + "name": "UQ_platform_integrations_linear_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'linear' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_user_bitbucket": { + "name": "UQ_platform_integrations_user_bitbucket", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_org_bitbucket": { + "name": "UQ_platform_integrations_org_bitbucket", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_id": { + "name": "IDX_platform_integrations_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_id": { + "name": "IDX_platform_integrations_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_inst_id": { + "name": "IDX_platform_integrations_platform_inst_id", + "columns": [ + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform": { + "name": "IDX_platform_integrations_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_platform": { + "name": "IDX_platform_integrations_owned_by_org_platform", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_platform": { + "name": "IDX_platform_integrations_owned_by_user_platform", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_integration_status": { + "name": "IDX_platform_integrations_integration_status", + "columns": [ + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_kilo_requester": { + "name": "IDX_platform_integrations_kilo_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_requester_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_requester": { + "name": "IDX_platform_integrations_platform_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_requester_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_integrations_owned_by_organization_id_organizations_id_fk": { + "name": "platform_integrations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_integrations_owned_by_user_id_kilocode_users_id_fk": { + "name": "platform_integrations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_integrations_owner_check": { + "name": "platform_integrations_owner_check", + "value": "(\n (\"platform_integrations\".\"owned_by_user_id\" IS NOT NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NULL) OR\n (\"platform_integrations\".\"owned_by_user_id\" IS NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.platform_oauth_credentials": { + "name": "platform_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject_login": { + "name": "provider_subject_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_oauth_credentials_platform_integration_id": { + "name": "UQ_platform_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_oauth_credentials_authorized_by_user_id": { + "name": "IDX_platform_oauth_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_code_usages": { + "name": "referral_code_usages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "referring_kilo_user_id": { + "name": "referring_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeeming_kilo_user_id": { + "name": "redeeming_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_referral_code_usages_redeeming_kilo_user_id": { + "name": "IDX_referral_code_usages_redeeming_kilo_user_id", + "columns": [ + { + "expression": "redeeming_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_referral_code_usages_redeeming_user_id_code": { + "name": "UQ_referral_code_usages_redeeming_user_id_code", + "nullsNotDistinct": false, + "columns": [ + "redeeming_kilo_user_id", + "referring_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_referral_codes_kilo_user_id": { + "name": "UQ_referral_codes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_referral_codes_code": { + "name": "IDX_referral_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_check_catalog": { + "name": "security_advisor_check_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_check_catalog_check_id_unique": { + "name": "security_advisor_check_catalog_check_id_unique", + "nullsNotDistinct": false, + "columns": [ + "check_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "security_advisor_check_catalog_severity_check": { + "name": "security_advisor_check_catalog_severity_check", + "value": "\"security_advisor_check_catalog\".\"severity\" in ('critical', 'warn', 'info')" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_content": { + "name": "security_advisor_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_content_key_unique": { + "name": "security_advisor_content_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_kiloclaw_coverage": { + "name": "security_advisor_kiloclaw_coverage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_check_ids": { + "name": "match_check_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_kiloclaw_coverage_area_unique": { + "name": "security_advisor_kiloclaw_coverage_area_unique", + "nullsNotDistinct": false, + "columns": [ + "area" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_scans": { + "name": "security_advisor_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_platform": { + "name": "source_platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_method": { + "name": "source_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_ip": { + "name": "public_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "findings_critical": { + "name": "findings_critical", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_warn": { + "name": "findings_warn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_info": { + "name": "findings_info", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_advisor_scans_user_created_at": { + "name": "idx_security_advisor_scans_user_created_at", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_created_at": { + "name": "idx_security_advisor_scans_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_platform": { + "name": "idx_security_advisor_scans_platform", + "columns": [ + { + "expression": "source_platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_agent_commands": { + "name": "security_agent_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'accepted'" + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_agent_commands_org_created": { + "name": "idx_security_agent_commands_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_user_created": { + "name": "idx_security_agent_commands_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_status_updated": { + "name": "idx_security_agent_commands_status_updated", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_finding_created": { + "name": "idx_security_agent_commands_finding_created", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_commands_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_commands_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_commands_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_finding_id_security_findings_id_fk": { + "name": "security_agent_commands_finding_id_security_findings_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_commands_owner_check": { + "name": "security_agent_commands_owner_check", + "value": "(\n (\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_commands\".\"owned_by_user_id\" IS NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_agent_commands_type_check": { + "name": "security_agent_commands_type_check", + "value": "\"security_agent_commands\".\"command_type\" IN ('sync', 'dismiss_finding', 'start_analysis', 'apply_auto_remediation')" + }, + "security_agent_commands_origin_check": { + "name": "security_agent_commands_origin_check", + "value": "\"security_agent_commands\".\"origin\" IN ('manual', 'dashboard_refresh', 'enable_initial_sync', 'settings_include_existing')" + }, + "security_agent_commands_status_check": { + "name": "security_agent_commands_status_check", + "value": "\"security_agent_commands\".\"status\" IN ('accepted', 'running', 'succeeded', 'failed', 'no_op')" + } + }, + "isRLSEnabled": false + }, + "public.security_agent_repository_sync_state": { + "name": "security_agent_repository_sync_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_succeeded_at": { + "name": "last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_agent_repository_sync_state_org_repo": { + "name": "UQ_security_agent_repository_sync_state_org_repo", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_repository_sync_state_user_repo": { + "name": "UQ_security_agent_repository_sync_state_user_repo", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_repository_sync_state_owner_check": { + "name": "security_agent_repository_sync_state_owner_check", + "value": "(\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_owner_state": { + "name": "security_analysis_owner_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_analysis_enabled_at": { + "name": "auto_analysis_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "block_reason": { + "name": "block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_actor_resolution_failures": { + "name": "consecutive_actor_resolution_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_actor_resolution_failure_at": { + "name": "last_actor_resolution_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_owner_state_org_owner": { + "name": "UQ_security_analysis_owner_state_org_owner", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_analysis_owner_state_user_owner": { + "name": "UQ_security_analysis_owner_state_user_owner", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_owner_state_owner_check": { + "name": "security_analysis_owner_state_owner_check", + "value": "(\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_owner_state_block_reason_check": { + "name": "security_analysis_owner_state_block_reason_check", + "value": "\"security_analysis_owner_state\".\"block_reason\" IS NULL OR \"security_analysis_owner_state\".\"block_reason\" IN ('INSUFFICIENT_CREDITS', 'ACTOR_RESOLUTION_FAILED', 'OPERATOR_PAUSE')" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_queue": { + "name": "security_analysis_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queue_status": { + "name": "queue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity_rank": { + "name": "severity_rank", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reopen_requeue_count": { + "name": "reopen_requeue_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_queue_finding_id": { + "name": "UQ_security_analysis_queue_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_org": { + "name": "idx_security_analysis_queue_claim_path_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_user": { + "name": "idx_security_analysis_queue_claim_path_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_org": { + "name": "idx_security_analysis_queue_in_flight_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_user": { + "name": "idx_security_analysis_queue_in_flight_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_lag_dashboards": { + "name": "idx_security_analysis_queue_lag_dashboards", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_pending_reconciliation": { + "name": "idx_security_analysis_queue_pending_reconciliation", + "columns": [ + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_running_reconciliation": { + "name": "idx_security_analysis_queue_running_reconciliation", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_failure_trend": { + "name": "idx_security_analysis_queue_failure_trend", + "columns": [ + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"failure_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_queue_finding_id_security_findings_id_fk": { + "name": "security_analysis_queue_finding_id_security_findings_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_queue_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_queue_owner_check": { + "name": "security_analysis_queue_owner_check", + "value": "(\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_queue_status_check": { + "name": "security_analysis_queue_status_check", + "value": "\"security_analysis_queue\".\"queue_status\" IN ('queued', 'pending', 'running', 'failed', 'completed')" + }, + "security_analysis_queue_claim_token_required_check": { + "name": "security_analysis_queue_claim_token_required_check", + "value": "\"security_analysis_queue\".\"queue_status\" NOT IN ('pending', 'running') OR \"security_analysis_queue\".\"claim_token\" IS NOT NULL" + }, + "security_analysis_queue_attempt_count_non_negative_check": { + "name": "security_analysis_queue_attempt_count_non_negative_check", + "value": "\"security_analysis_queue\".\"attempt_count\" >= 0" + }, + "security_analysis_queue_reopen_requeue_count_non_negative_check": { + "name": "security_analysis_queue_reopen_requeue_count_non_negative_check", + "value": "\"security_analysis_queue\".\"reopen_requeue_count\" >= 0" + }, + "security_analysis_queue_severity_rank_check": { + "name": "security_analysis_queue_severity_rank_check", + "value": "\"security_analysis_queue\".\"severity_rank\" IN (0, 1, 2, 3)" + }, + "security_analysis_queue_failure_code_check": { + "name": "security_analysis_queue_failure_code_check", + "value": "\"security_analysis_queue\".\"failure_code\" IS NULL OR \"security_analysis_queue\".\"failure_code\" IN (\n 'NETWORK_TIMEOUT',\n 'UPSTREAM_5XX',\n 'TEMP_TOKEN_FAILURE',\n 'START_CALL_AMBIGUOUS',\n 'REQUEUE_TEMPORARY_PRECONDITION',\n 'ACTOR_RESOLUTION_FAILED',\n 'GITHUB_TOKEN_UNAVAILABLE',\n 'INVALID_CONFIG',\n 'MISSING_OWNERSHIP',\n 'PERMISSION_DENIED_PERMANENT',\n 'UNSUPPORTED_SEVERITY',\n 'INSUFFICIENT_CREDITS',\n 'STATE_GUARD_REJECTED',\n 'SKIPPED_ALREADY_IN_PROGRESS',\n 'SKIPPED_NO_LONGER_ELIGIBLE',\n 'REOPEN_LOOP_GUARD',\n 'RUN_LOST'\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_audit_log": { + "name": "security_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source_occurred_at": { + "name": "source_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "finding_snapshot": { + "name": "finding_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_context": { + "name": "source_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_security_audit_log_org_created": { + "name": "IDX_security_audit_log_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_created": { + "name": "IDX_security_audit_log_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_resource": { + "name": "IDX_security_audit_log_resource", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_actor": { + "name": "IDX_security_audit_log_actor", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_action": { + "name": "IDX_security_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_org_event_key": { + "name": "UQ_security_audit_log_org_event_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_user_event_key": { + "name": "UQ_security_audit_log_user_event_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_org_occurred": { + "name": "IDX_security_audit_log_org_occurred", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_occurred": { + "name": "IDX_security_audit_log_user_occurred", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_audit_log_owned_by_organization_id_organizations_id_fk": { + "name": "security_audit_log_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_audit_log_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_audit_log_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_audit_log_owner_check": { + "name": "security_audit_log_owner_check", + "value": "(\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NULL) OR (\"security_audit_log\".\"owned_by_user_id\" IS NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "security_audit_log_action_check": { + "name": "security_audit_log_action_check", + "value": "\"security_audit_log\".\"action\" IN ('security.finding.created', 'security.finding.severity_changed', 'security.finding.status_change', 'security.finding.dismissed', 'security.finding.auto_dismissed', 'security.finding.superseded', 'security.finding.analysis_started', 'security.finding.analysis_completed', 'security.finding.analysis_failed', 'security.remediation.queued', 'security.remediation.started', 'security.remediation.pr_opened', 'security.remediation.failed', 'security.remediation.blocked', 'security.remediation.no_changes_needed', 'security.remediation.cancelled', 'security.remediation.retried', 'security.finding.deleted', 'security.config.enabled', 'security.config.disabled', 'security.config.updated', 'security.sync.triggered', 'security.sync.completed', 'security.audit_log.exported', 'security.audit_report.generated')" + }, + "security_audit_log_actor_type_check": { + "name": "security_audit_log_actor_type_check", + "value": "\"security_audit_log\".\"actor_type\" IN ('customer_user', 'kilo_admin', 'system')" + }, + "security_audit_log_source_context_check": { + "name": "security_audit_log_source_context_check", + "value": "\"security_audit_log\".\"source_context\" IN ('security_sync', 'web', 'analysis_worker', 'remediation_callback', 'rollout_baseline')" + } + }, + "isRLSEnabled": false + }, + "public.security_finding_notifications": { + "name": "security_finding_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'staged'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_finding_notifications_finding_recipient_kind": { + "name": "uq_security_finding_notifications_finding_recipient_kind", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_pending": { + "name": "idx_security_finding_notifications_pending", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_staged": { + "name": "idx_security_finding_notifications_staged", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'staged'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_finding_id": { + "name": "idx_security_finding_notifications_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_recipient_user_id": { + "name": "idx_security_finding_notifications_recipient_user_id", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_finding_notifications_finding_fk": { + "name": "security_finding_notifications_finding_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_finding_notifications_recipient_fk": { + "name": "security_finding_notifications_recipient_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_finding_notifications_kind_check": { + "name": "security_finding_notifications_kind_check", + "value": "\"security_finding_notifications\".\"kind\" IN ('new_finding', 'sla_warning', 'sla_breach')" + }, + "security_finding_notifications_status_check": { + "name": "security_finding_notifications_status_check", + "value": "\"security_finding_notifications\".\"status\" IN ('staged', 'pending', 'sending', 'sent', 'failed', 'cancelled')" + }, + "security_finding_notifications_attempt_count_check": { + "name": "security_finding_notifications_attempt_count_check", + "value": "\"security_finding_notifications\".\"attempt_count\" >= 0" + }, + "security_finding_notifications_claimed_at_check": { + "name": "security_finding_notifications_claimed_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NULL)\n )" + }, + "security_finding_notifications_sent_at_check": { + "name": "security_finding_notifications_sent_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NULL)\n )" + }, + "security_finding_notifications_error_message_length_check": { + "name": "security_finding_notifications_error_message_length_check", + "value": "\"security_finding_notifications\".\"error_message\" IS NULL OR length(\"security_finding_notifications\".\"error_message\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.security_findings": { + "name": "security_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ghsa_id": { + "name": "ghsa_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cve_id": { + "name": "cve_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_ecosystem": { + "name": "package_ecosystem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vulnerable_version_range": { + "name": "vulnerable_version_range", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patched_version": { + "name": "patched_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest_path": { + "name": "manifest_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "ignored_reason": { + "name": "ignored_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignored_by": { + "name": "ignored_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fixed_at": { + "name": "fixed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sla_due_at": { + "name": "sla_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dependabot_html_url": { + "name": "dependabot_html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwe_ids": { + "name": "cwe_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cvss_score": { + "name": "cvss_score", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": false + }, + "dependency_scope": { + "name": "dependency_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_status": { + "name": "analysis_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_started_at": { + "name": "analysis_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_error": { + "name": "analysis_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis": { + "name": "analysis", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_detected_at": { + "name": "first_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_findings_user_source": { + "name": "uq_security_findings_user_source", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_security_findings_org_source": { + "name": "uq_security_findings_org_source", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_id": { + "name": "idx_security_findings_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_id": { + "name": "idx_security_findings_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_repo": { + "name": "idx_security_findings_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_severity": { + "name": "idx_security_findings_severity", + "columns": [ + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_status": { + "name": "idx_security_findings_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_package": { + "name": "idx_security_findings_package", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_sla_due_at": { + "name": "idx_security_findings_sla_due_at", + "columns": [ + { + "expression": "sla_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_session_id": { + "name": "idx_security_findings_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_cli_session_id": { + "name": "idx_security_findings_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_analysis_status": { + "name": "idx_security_findings_analysis_status", + "columns": [ + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_analysis_in_flight": { + "name": "idx_security_findings_org_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_analysis_in_flight": { + "name": "idx_security_findings_user_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_findings_owned_by_organization_id_organizations_id_fk": { + "name": "security_findings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_findings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_findings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_findings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_platform_integration_id_platform_integrations_id_fk": { + "name": "security_findings_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "security_findings", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_findings_owner_check": { + "name": "security_findings_owner_check", + "value": "(\n (\"security_findings\".\"owned_by_user_id\" IS NOT NULL AND \"security_findings\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_findings\".\"owned_by_user_id\" IS NULL AND \"security_findings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_remediation_attempts": { + "name": "security_remediation_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "remediation_id": { + "name": "remediation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_fingerprint": { + "name": "analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "remediation_model_slug": { + "name": "remediation_model_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_attempt_count": { + "name": "launch_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "callback_attempt_token_hash": { + "name": "callback_attempt_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_result": { + "name": "structured_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "final_assistant_message": { + "name": "final_assistant_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "validation_evidence": { + "name": "validation_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "risk_notes": { + "name": "risk_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_reason": { + "name": "draft_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_by_user_id": { + "name": "cancellation_requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "launched_at": { + "name": "launched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediation_attempts_number": { + "name": "UQ_security_remediation_attempts_number", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_finding": { + "name": "UQ_security_remediation_attempts_active_finding", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_remediation": { + "name": "UQ_security_remediation_attempts_active_remediation", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_finding_fingerprint_terminal": { + "name": "UQ_security_remediation_attempts_finding_fingerprint_terminal", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_claim": { + "name": "idx_security_remediation_attempts_org_claim", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_claim": { + "name": "idx_security_remediation_attempts_user_claim", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_claim": { + "name": "idx_security_remediation_attempts_repo_claim", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_inflight": { + "name": "idx_security_remediation_attempts_org_inflight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_inflight": { + "name": "idx_security_remediation_attempts_user_inflight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_inflight": { + "name": "idx_security_remediation_attempts_repo_inflight", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_cloud_agent_session": { + "name": "idx_security_remediation_attempts_cloud_agent_session", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_finding_fingerprint": { + "name": "idx_security_remediation_attempts_finding_fingerprint", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediation_attempts_remediation_id_security_remediations_id_fk": { + "name": "security_remediation_attempts_remediation_id_security_remediations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_remediations", + "columnsFrom": [ + "remediation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_finding_id_security_findings_id_fk": { + "name": "security_remediation_attempts_finding_id_security_findings_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediation_attempts_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "cancellation_requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediation_attempts_owner_check": { + "name": "security_remediation_attempts_owner_check", + "value": "(\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediation_attempts_status_check": { + "name": "security_remediation_attempts_status_check", + "value": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + }, + "security_remediation_attempts_origin_check": { + "name": "security_remediation_attempts_origin_check", + "value": "\"security_remediation_attempts\".\"origin\" IN ('auto_policy', 'bulk_existing', 'manual')" + }, + "security_remediation_attempts_attempt_number_check": { + "name": "security_remediation_attempts_attempt_number_check", + "value": "\"security_remediation_attempts\".\"attempt_number\" >= 1" + }, + "security_remediation_attempts_launch_attempt_count_check": { + "name": "security_remediation_attempts_launch_attempt_count_check", + "value": "\"security_remediation_attempts\".\"launch_attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.security_remediations": { + "name": "security_remediations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "latest_attempt_id": { + "name": "latest_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_fingerprint": { + "name": "latest_analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_completed_at": { + "name": "latest_analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_summary": { + "name": "outcome_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediations_finding_id": { + "name": "UQ_security_remediations_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_org_status": { + "name": "idx_security_remediations_org_status", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_user_status": { + "name": "idx_security_remediations_user_status", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_repo_status": { + "name": "idx_security_remediations_repo_status", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_latest_attempt": { + "name": "idx_security_remediations_latest_attempt", + "columns": [ + { + "expression": "latest_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediations_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_finding_id_security_findings_id_fk": { + "name": "security_remediations_finding_id_security_findings_id_fk", + "tableFrom": "security_remediations", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediations_owner_check": { + "name": "security_remediations_owner_check", + "value": "(\n (\"security_remediations\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediations\".\"owned_by_user_id\" IS NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediations_status_check": { + "name": "security_remediations_status_check", + "value": "\"security_remediations\".\"status\" IN ('queued', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.shared_cli_sessions": { + "name": "shared_cli_sessions", + "schema": "", + "columns": { + "share_id": { + "name": "share_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_state": { + "name": "shared_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_shared_cli_sessions_session_id": { + "name": "IDX_shared_cli_sessions_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_shared_cli_sessions_created_at": { + "name": "IDX_shared_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_cli_sessions_session_id_cli_sessions_session_id_fk": { + "name": "shared_cli_sessions_session_id_cli_sessions_session_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_cli_sessions_shared_state_check": { + "name": "shared_cli_sessions_shared_state_check", + "value": "\"shared_cli_sessions\".\"shared_state\" IN ('public', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.slack_bot_requests": { + "name": "slack_bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_name": { + "name": "slack_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message_truncated": { + "name": "user_message_truncated", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_calls_made": { + "name": "tool_calls_made", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_slack_bot_requests_created_at": { + "name": "idx_slack_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_slack_team_id": { + "name": "idx_slack_bot_requests_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_org_id": { + "name": "idx_slack_bot_requests_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_user_id": { + "name": "idx_slack_bot_requests_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_status": { + "name": "idx_slack_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_event_type": { + "name": "idx_slack_bot_requests_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_team_created": { + "name": "idx_slack_bot_requests_team_created", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_bot_requests_owned_by_organization_id_organizations_id_fk": { + "name": "slack_bot_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_bot_requests_owner_check": { + "name": "slack_bot_requests_owner_check", + "value": "(\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NOT NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NOT NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.source_embeddings": { + "name": "source_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_base_branch": { + "name": "is_base_branch", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_source_embeddings_organization_id": { + "name": "IDX_source_embeddings_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_kilo_user_id": { + "name": "IDX_source_embeddings_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_project_id": { + "name": "IDX_source_embeddings_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_created_at": { + "name": "IDX_source_embeddings_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_updated_at": { + "name": "IDX_source_embeddings_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_file_path_lower": { + "name": "IDX_source_embeddings_file_path_lower", + "columns": [ + { + "expression": "LOWER(\"file_path\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_git_branch": { + "name": "IDX_source_embeddings_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_org_project_branch": { + "name": "IDX_source_embeddings_org_project_branch", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "source_embeddings_organization_id_organizations_id_fk": { + "name": "source_embeddings_organization_id_organizations_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "source_embeddings_kilo_user_id_kilocode_users_id_fk": { + "name": "source_embeddings_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_source_embeddings_org_project_branch_file_lines": { + "name": "UQ_source_embeddings_org_project_branch_file_lines", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "project_id", + "git_branch", + "file_path", + "start_line", + "end_line" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stripe_dispute_actions": { + "name": "stripe_dispute_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_actions_case_id": { + "name": "IDX_stripe_dispute_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_actions_claim_path": { + "name": "IDX_stripe_dispute_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk": { + "name": "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk", + "tableFrom": "stripe_dispute_actions", + "tableTo": "stripe_dispute_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_actions_case_type_target": { + "name": "UQ_stripe_dispute_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_actions_action_type_check": { + "name": "stripe_dispute_actions_action_type_check", + "value": "\"stripe_dispute_actions\".\"action_type\" IN ('stripe_acceptance', 'user_block', 'auto_top_up_disable', 'credit_balance_reset', 'subscription_cancellation', 'access_termination', 'kiloclaw_suspension')" + }, + "stripe_dispute_actions_status_check": { + "name": "stripe_dispute_actions_status_check", + "value": "\"stripe_dispute_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'skipped')" + }, + "stripe_dispute_actions_attempt_count_non_negative_check": { + "name": "stripe_dispute_actions_attempt_count_non_negative_check", + "value": "\"stripe_dispute_actions\".\"attempt_count\" >= 0" + }, + "stripe_dispute_actions_target_key_not_empty_check": { + "name": "stripe_dispute_actions_target_key_not_empty_check", + "value": "length(\"stripe_dispute_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_dispute_cases": { + "name": "stripe_dispute_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_dispute_id": { + "name": "stripe_dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_created_at": { + "name": "stripe_event_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispute_reason": { + "name": "dispute_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_status": { + "name": "stripe_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'needs_action'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_created_at": { + "name": "stripe_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evidence_due_by": { + "name": "evidence_due_by", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by_kilo_user_id": { + "name": "accepted_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance_started_at": { + "name": "acceptance_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enforcement_completed_at": { + "name": "enforcement_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_cases_event_id": { + "name": "IDX_stripe_dispute_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_charge_id": { + "name": "IDX_stripe_dispute_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_payment_intent_id": { + "name": "IDX_stripe_dispute_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_customer_id": { + "name": "IDX_stripe_dispute_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_kilo_user_id": { + "name": "IDX_stripe_dispute_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_organization_id": { + "name": "IDX_stripe_dispute_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_status_due_by": { + "name": "IDX_stripe_dispute_cases_status_due_by", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "evidence_due_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_organization_id_organizations_id_fk": { + "name": "stripe_dispute_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "accepted_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_cases_dispute_id": { + "name": "UQ_stripe_dispute_cases_dispute_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_dispute_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_cases_owner_classification_check": { + "name": "stripe_dispute_cases_owner_classification_check", + "value": "\"stripe_dispute_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_dispute_cases_status_check": { + "name": "stripe_dispute_cases_status_check", + "value": "\"stripe_dispute_cases\".\"status\" IN ('needs_action', 'processing', 'accepted', 'acceptance_failed', 'enforcement_failed', 'review_required', 'closed')" + }, + "stripe_dispute_cases_amount_minor_units_non_negative_check": { + "name": "stripe_dispute_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_dispute_cases\".\"amount_minor_units\" IS NULL OR \"stripe_dispute_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_actions": { + "name": "stripe_early_fraud_warning_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_actions_case_id": { + "name": "IDX_stripe_early_fraud_warning_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_actions_claim_path": { + "name": "IDX_stripe_early_fraud_warning_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk": { + "name": "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk", + "tableFrom": "stripe_early_fraud_warning_actions", + "tableTo": "stripe_early_fraud_warning_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_actions_case_type_target": { + "name": "UQ_stripe_early_fraud_warning_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_actions_action_type_check": { + "name": "stripe_early_fraud_warning_actions_action_type_check", + "value": "\"stripe_early_fraud_warning_actions\".\"action_type\" IN ('containment', 'refund', 'payment_value_clawback', 'subscription_termination', 'access_termination', 'kiloclaw_suspension', 'affiliate_payout_reversal', 'referral_reward_reversal', 'user_notice')" + }, + "stripe_early_fraud_warning_actions_status_check": { + "name": "stripe_early_fraud_warning_actions_status_check", + "value": "\"stripe_early_fraud_warning_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'review_required', 'dismissed')" + }, + "stripe_early_fraud_warning_actions_attempt_count_non_negative_check": { + "name": "stripe_early_fraud_warning_actions_attempt_count_non_negative_check", + "value": "\"stripe_early_fraud_warning_actions\".\"attempt_count\" >= 0" + }, + "stripe_early_fraud_warning_actions_target_key_not_empty_check": { + "name": "stripe_early_fraud_warning_actions_target_key_not_empty_check", + "value": "length(\"stripe_early_fraud_warning_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_cases": { + "name": "stripe_early_fraud_warning_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_early_fraud_warning_id": { + "name": "stripe_early_fraud_warning_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warning_created_at": { + "name": "warning_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "contained_at": { + "name": "contained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remediated_at": { + "name": "remediated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_cases_event_id": { + "name": "IDX_stripe_early_fraud_warning_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_charge_id": { + "name": "IDX_stripe_early_fraud_warning_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_payment_intent_id": { + "name": "IDX_stripe_early_fraud_warning_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_customer_id": { + "name": "IDX_stripe_early_fraud_warning_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_kilo_user_id": { + "name": "IDX_stripe_early_fraud_warning_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_organization_id": { + "name": "IDX_stripe_early_fraud_warning_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_status_created_at": { + "name": "IDX_stripe_early_fraud_warning_cases_status_created_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk": { + "name": "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_cases_warning_id": { + "name": "UQ_stripe_early_fraud_warning_cases_warning_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_early_fraud_warning_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_cases_owner_classification_check": { + "name": "stripe_early_fraud_warning_cases_owner_classification_check", + "value": "\"stripe_early_fraud_warning_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_early_fraud_warning_cases_status_check": { + "name": "stripe_early_fraud_warning_cases_status_check", + "value": "\"stripe_early_fraud_warning_cases\".\"status\" IN ('queued', 'contained', 'processing', 'completed', 'review_required', 'failed', 'remediated', 'dismissed')" + }, + "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check": { + "name": "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_early_fraud_warning_cases\".\"amount_minor_units\" IS NULL OR \"stripe_early_fraud_warning_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stytch_fingerprints": { + "name": "stytch_fingerprints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_fingerprint": { + "name": "visitor_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_fingerprint": { + "name": "browser_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_id": { + "name": "browser_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hardware_fingerprint": { + "name": "hardware_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network_fingerprint": { + "name": "network_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_id": { + "name": "visitor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verdict_action": { + "name": "verdict_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_device_type": { + "name": "detected_device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_authentic_device": { + "name": "is_authentic_device", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"\"}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fingerprint_data": { + "name": "fingerprint_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_free_tier_allowed": { + "name": "kilo_free_tier_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_hardware_fingerprint": { + "name": "idx_hardware_fingerprint", + "columns": [ + { + "expression": "hardware_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id": { + "name": "idx_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_stytch_fingerprints_reasons_gin": { + "name": "idx_stytch_fingerprints_reasons_gin", + "columns": [ + { + "expression": "reasons", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_verdict_action": { + "name": "idx_verdict_action", + "columns": [ + { + "expression": "verdict_action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_visitor_fingerprint": { + "name": "idx_visitor_fingerprint", + "columns": [ + { + "expression": "visitor_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_prompt_prefix": { + "name": "system_prompt_prefix", + "schema": "", + "columns": { + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_system_prompt_prefix": { + "name": "UQ_system_prompt_prefix", + "columns": [ + { + "expression": "system_prompt_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactional_email_log": { + "name": "transactional_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_transactional_email_log_type_idempotency_key": { + "name": "UQ_transactional_email_log_type_idempotency_key", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_user_id": { + "name": "IDX_transactional_email_log_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_organization_id": { + "name": "IDX_transactional_email_log_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactional_email_log_user_id_kilocode_users_id_fk": { + "name": "transactional_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactional_email_log_organization_id_organizations_id_fk": { + "name": "transactional_email_log_organization_id_organizations_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_transactional_email_log_owner": { + "name": "CHK_transactional_email_log_owner", + "value": "\"transactional_email_log\".\"user_id\" IS NOT NULL OR \"transactional_email_log\".\"organization_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.user_admin_notes": { + "name": "user_admin_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note_content": { + "name": "note_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_kilo_user_id": { + "name": "admin_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_34517df0b385234babc38fe81b": { + "name": "IDX_34517df0b385234babc38fe81b", + "columns": [ + { + "expression": "admin_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_ccbde98c4c14046daa5682ec4f": { + "name": "IDX_ccbde98c4c14046daa5682ec4f", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_d0270eb24ef6442d65a0b7853c": { + "name": "IDX_d0270eb24ef6442d65a0b7853c", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_affiliate_attributions": { + "name": "user_affiliate_attributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_id": { + "name": "tracking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_attributions_user_id": { + "name": "IDX_user_affiliate_attributions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_attributions_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_attributions_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_attributions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_attributions_user_provider": { + "name": "UQ_user_affiliate_attributions_user_provider", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_attributions_provider_check": { + "name": "user_affiliate_attributions_provider_check", + "value": "\"user_affiliate_attributions\".\"provider\" IN ('impact')" + } + }, + "isRLSEnabled": false + }, + "public.user_affiliate_events": { + "name": "user_affiliate_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_event_id": { + "name": "parent_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_action_id": { + "name": "impact_action_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_submission_uri": { + "name": "impact_submission_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_events_claim_path": { + "name": "IDX_user_affiliate_events_claim_path", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_parent_event_id": { + "name": "IDX_user_affiliate_events_parent_event_id", + "columns": [ + { + "expression": "parent_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_provider_event_type_charge": { + "name": "IDX_user_affiliate_events_provider_event_type_charge", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_events_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_events_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_affiliate_events_parent_event_id_fk": { + "name": "user_affiliate_events_parent_event_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "user_affiliate_events", + "columnsFrom": [ + "parent_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_events_dedupe_key": { + "name": "UQ_user_affiliate_events_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_events_provider_check": { + "name": "user_affiliate_events_provider_check", + "value": "\"user_affiliate_events\".\"provider\" IN ('impact')" + }, + "user_affiliate_events_event_type_check": { + "name": "user_affiliate_events_event_type_check", + "value": "\"user_affiliate_events\".\"event_type\" IN ('signup', 'trial_start', 'trial_end', 'sale', 'sale_reversal')" + }, + "user_affiliate_events_delivery_state_check": { + "name": "user_affiliate_events_delivery_state_check", + "value": "\"user_affiliate_events\".\"delivery_state\" IN ('queued', 'blocked', 'sending', 'delivered', 'failed')" + }, + "user_affiliate_events_attempt_count_non_negative_check": { + "name": "user_affiliate_events_attempt_count_non_negative_check", + "value": "\"user_affiliate_events\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_auth_provider": { + "name": "user_auth_provider", + "schema": "", + "columns": { + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_auth_provider_kilo_user_id": { + "name": "IDX_user_auth_provider_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_hosted_domain": { + "name": "IDX_user_auth_provider_hosted_domain", + "columns": [ + { + "expression": "hosted_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_auth_provider_provider_provider_account_id_pk": { + "name": "user_auth_provider_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_feedback": { + "name": "user_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feedback_for": { + "name": "feedback_for", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "feedback_batch": { + "name": "feedback_batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_feedback_created_at": { + "name": "IDX_user_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_kilo_user_id": { + "name": "IDX_user_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_for": { + "name": "IDX_user_feedback_feedback_for", + "columns": [ + { + "expression": "feedback_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_batch": { + "name": "IDX_user_feedback_feedback_batch", + "columns": [ + { + "expression": "feedback_batch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_source": { + "name": "IDX_user_feedback_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "user_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_github_app_tokens": { + "name": "user_github_app_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_github_app_tokens_user_app": { + "name": "UQ_user_github_app_tokens_user_app", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_github_app_tokens_github_user_app": { + "name": "UQ_user_github_app_tokens_github_user_app", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_github_app_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_github_app_tokens_app_type_check": { + "name": "user_github_app_tokens_app_type_check", + "value": "\"user_github_app_tokens\".\"github_app_type\" IN ('standard', 'lite')" + } + }, + "isRLSEnabled": false + }, + "public.user_period_cache": { + "name": "user_period_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cache_type": { + "name": "cache_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_type": { + "name": "period_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_key": { + "name": "period_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "shared_url_token": { + "name": "shared_url_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_user_period_cache_kilo_user_id": { + "name": "IDX_user_period_cache_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_period_cache": { + "name": "UQ_user_period_cache", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cache_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_period_cache_lookup": { + "name": "IDX_user_period_cache_lookup", + "columns": [ + { + "expression": "cache_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_period_cache_share_token": { + "name": "UQ_user_period_cache_share_token", + "columns": [ + { + "expression": "shared_url_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_period_cache\".\"shared_url_token\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_period_cache_kilo_user_id_kilocode_users_id_fk": { + "name": "user_period_cache_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_period_cache", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_period_cache_period_type_check": { + "name": "user_period_cache_period_type_check", + "value": "\"user_period_cache\".\"period_type\" IN ('year', 'quarter', 'month', 'week', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.user_push_tokens": { + "name": "user_push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_push_tokens_token": { + "name": "UQ_user_push_tokens_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_push_tokens_user_id": { + "name": "IDX_user_push_tokens_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_push_tokens_user_id_kilocode_users_id_fk": { + "name": "user_push_tokens_user_id_kilocode_users_id_fk", + "tableFrom": "user_push_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_city": { + "name": "vercel_ip_city", + "schema": "", + "columns": { + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_city": { + "name": "vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_city": { + "name": "UQ_vercel_ip_city", + "columns": [ + { + "expression": "vercel_ip_city", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_country": { + "name": "vercel_ip_country", + "schema": "", + "columns": { + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_country": { + "name": "vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_country": { + "name": "UQ_vercel_ip_country", + "columns": [ + { + "expression": "vercel_ip_country", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_events": { + "name": "webhook_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_action": { + "name": "event_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "handlers_triggered": { + "name": "handlers_triggered", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "event_signature": { + "name": "event_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_webhook_events_owned_by_org_id": { + "name": "IDX_webhook_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_owned_by_user_id": { + "name": "IDX_webhook_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_platform": { + "name": "IDX_webhook_events_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_event_type": { + "name": "IDX_webhook_events_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_created_at": { + "name": "IDX_webhook_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_events_owned_by_organization_id_organizations_id_fk": { + "name": "webhook_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "webhook_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "webhook_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "webhook_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_webhook_events_signature": { + "name": "UQ_webhook_events_signature", + "nullsNotDistinct": false, + "columns": [ + "event_signature" + ] + } + }, + "policies": {}, + "checkConstraints": { + "webhook_events_owner_check": { + "name": "webhook_events_owner_check", + "value": "(\n (\"webhook_events\".\"owned_by_user_id\" IS NOT NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"webhook_events\".\"owned_by_user_id\" IS NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.microdollar_usage_view": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n mu.id,\n mu.kilo_user_id,\n meta.message_id,\n mu.cost,\n mu.input_tokens,\n mu.output_tokens,\n mu.cache_write_tokens,\n mu.cache_hit_tokens,\n mu.created_at,\n ip.http_ip AS http_x_forwarded_for,\n city.vercel_ip_city AS http_x_vercel_ip_city,\n country.vercel_ip_country AS http_x_vercel_ip_country,\n meta.vercel_ip_latitude AS http_x_vercel_ip_latitude,\n meta.vercel_ip_longitude AS http_x_vercel_ip_longitude,\n ja4.ja4_digest AS http_x_vercel_ja4_digest,\n mu.provider,\n mu.model,\n mu.requested_model,\n meta.user_prompt_prefix,\n spp.system_prompt_prefix,\n meta.system_prompt_length,\n ua.http_user_agent,\n mu.cache_discount,\n meta.max_tokens,\n meta.has_middle_out_transform,\n mu.has_error,\n mu.abuse_classification,\n mu.organization_id,\n mu.inference_provider,\n mu.project_id,\n meta.status_code,\n meta.upstream_id,\n frfr.finish_reason,\n meta.latency,\n meta.moderation_latency,\n meta.generation_time,\n meta.is_byok,\n meta.is_user_byok,\n meta.streamed,\n meta.cancelled,\n edit.editor_name,\n ak.api_kind,\n meta.has_tools,\n meta.machine_id,\n feat.feature,\n meta.session_id,\n md.mode,\n am.auto_model,\n meta.market_cost,\n meta.is_free,\n meta.abuse_delay,\n meta.abuse_downgraded_from\n FROM \"microdollar_usage\" mu\n LEFT JOIN \"microdollar_usage_metadata\" meta ON mu.id = meta.id\n LEFT JOIN \"http_ip\" ip ON meta.http_ip_id = ip.http_ip_id\n LEFT JOIN \"vercel_ip_city\" city ON meta.vercel_ip_city_id = city.vercel_ip_city_id\n LEFT JOIN \"vercel_ip_country\" country ON meta.vercel_ip_country_id = country.vercel_ip_country_id\n LEFT JOIN \"ja4_digest\" ja4 ON meta.ja4_digest_id = ja4.ja4_digest_id\n LEFT JOIN \"system_prompt_prefix\" spp ON meta.system_prompt_prefix_id = spp.system_prompt_prefix_id\n LEFT JOIN \"http_user_agent\" ua ON meta.http_user_agent_id = ua.http_user_agent_id\n LEFT JOIN \"finish_reason\" frfr ON meta.finish_reason_id = frfr.finish_reason_id\n LEFT JOIN \"editor_name\" edit ON meta.editor_name_id = edit.editor_name_id\n LEFT JOIN \"api_kind\" ak ON meta.api_kind_id = ak.api_kind_id\n LEFT JOIN \"feature\" feat ON meta.feature_id = feat.feature_id\n LEFT JOIN \"mode\" md ON meta.mode_id = md.mode_id\n LEFT JOIN \"auto_model\" am ON meta.auto_model_id = am.auto_model_id\n", + "name": "microdollar_usage_view", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 819761c75c..cd21e35127 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1219,6 +1219,13 @@ "when": 1782381719017, "tag": "0173_lowly_venom", "breakpoints": true + }, + { + "idx": 174, + "version": "7", + "when": 1782477973153, + "tag": "0174_sleepy_virginia_dare", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema-types.ts b/packages/db/src/schema-types.ts index 07e3beec0f..e6a46e8e65 100644 --- a/packages/db/src/schema-types.ts +++ b/packages/db/src/schema-types.ts @@ -639,6 +639,8 @@ export type CostInsightEventType = (typeof CostInsightEventType)[keyof typeof Co export const CostInsightAlertKind = { Anomaly: 'anomaly', Threshold: 'threshold', + Threshold7Day: 'threshold_7d', + Threshold30Day: 'threshold_30d', } as const; export type CostInsightAlertKind = (typeof CostInsightAlertKind)[keyof typeof CostInsightAlertKind]; diff --git a/packages/db/src/schema.test.ts b/packages/db/src/schema.test.ts index ad144a8682..0c5d135cff 100644 --- a/packages/db/src/schema.test.ts +++ b/packages/db/src/schema.test.ts @@ -540,6 +540,18 @@ describe('database schema', () => { 'reconciliation_mismatch', 'late_source_data', ], + CostInsightEventType: [ + 'config_changed', + 'anomaly_alert', + 'threshold_crossed', + 'alert_reviewed', + 'suggestion_created', + 'suggestion_dismissed', + 'disabled', + ], + CostInsightAlertKind: ['anomaly', 'threshold', 'threshold_7d', 'threshold_30d'], + CostInsightSuggestionKind: ['coding_plan', 'kilo_pass'], + CostInsightNotificationStatus: ['pending', 'sending', 'sent', 'failed'], CodeReviewAnalyticsCaptureStatus: ['captured', 'missing', 'invalid', 'omitted'], CodeReviewAnalyticsChangeType: [ 'bug_fix', diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 8da550410d..1f6bde520c 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -2930,7 +2930,10 @@ export type CostInsightRollupDegradedInterval = export type CostInsightEventSnapshot = { thresholdMicrodollars?: number | null; + thresholdWindow?: 'rolling_24h' | 'rolling_7d' | 'rolling_30d'; rolling24HourMicrodollars?: number | null; + rolling7DayMicrodollars?: number | null; + rolling30DayMicrodollars?: number | null; currentHourVariableMicrodollars?: number | null; anomalyBaselineMicrodollars?: number | null; anomalyThresholdMicrodollars?: number | null; @@ -2945,11 +2948,19 @@ export type CostInsightEventSnapshot = { totalMicrodollars: number; spendRecordCount: number; }>; + topDriversWindow?: { + startInclusive: string; + endExclusive: string; + spendCategory?: CostInsightSpendCategory; + }; changedFields?: Record; settings?: { spendAlertsEnabled: boolean; + anomalyAlertsEnabled: boolean; costSuggestionsEnabled: boolean; spendThresholdMicrodollars: number | null; + spend7DayThresholdMicrodollars: number | null; + spend30DayThresholdMicrodollars: number | null; }; suggestion?: { suggestionKey: string; @@ -2976,8 +2987,11 @@ export const cost_insight_owner_configs = pgTable( onUpdate: 'cascade', }), spend_alerts_enabled: boolean().default(false).notNull(), + anomaly_alerts_enabled: boolean().default(true).notNull(), cost_suggestions_enabled: boolean().default(true).notNull(), spend_threshold_microdollars: bigint({ mode: 'number' }), + spend_7_day_threshold_microdollars: bigint({ mode: 'number' }), + spend_30_day_threshold_microdollars: bigint({ mode: 'number' }), spend_alerts_enabled_at: timestamp({ withTimezone: true, mode: 'string' }), created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), updated_at: timestamp({ withTimezone: true, mode: 'string' }) @@ -3007,6 +3021,22 @@ export const cost_insight_owner_configs = pgTable( 'cost_insight_owner_configs_threshold_safe_check', sql`${table.spend_threshold_microdollars} IS NULL OR ${table.spend_threshold_microdollars} <= 9007199254740991` ), + check( + 'cost_insight_owner_configs_7_day_threshold_positive_check', + sql`${table.spend_7_day_threshold_microdollars} IS NULL OR ${table.spend_7_day_threshold_microdollars} > 0` + ), + check( + 'cost_insight_owner_configs_7_day_threshold_safe_check', + sql`${table.spend_7_day_threshold_microdollars} IS NULL OR ${table.spend_7_day_threshold_microdollars} <= 9007199254740991` + ), + check( + 'cost_insight_owner_configs_30_day_threshold_positive_check', + sql`${table.spend_30_day_threshold_microdollars} IS NULL OR ${table.spend_30_day_threshold_microdollars} > 0` + ), + check( + 'cost_insight_owner_configs_30_day_threshold_safe_check', + sql`${table.spend_30_day_threshold_microdollars} IS NULL OR ${table.spend_30_day_threshold_microdollars} <= 9007199254740991` + ), check( 'cost_insight_owner_configs_enabled_at_check', sql`${table.spend_alerts_enabled} = TRUE OR ${table.spend_alerts_enabled_at} IS NULL` @@ -3201,6 +3231,26 @@ export const cost_insight_owner_states = pgTable( threshold_crossing_started_at: timestamp({ withTimezone: true, mode: 'string' }), threshold_reviewed_at: timestamp({ withTimezone: true, mode: 'string' }), threshold_recovered_at: timestamp({ withTimezone: true, mode: 'string' }), + rolling_7_day_threshold_crossing_active: boolean().default(false).notNull(), + active_rolling_7_day_threshold_event_id: uuid().references(() => cost_insight_events.id, { + onDelete: 'set null', + }), + rolling_7_day_threshold_crossing_started_at: timestamp({ + withTimezone: true, + mode: 'string', + }), + rolling_7_day_threshold_reviewed_at: timestamp({ withTimezone: true, mode: 'string' }), + rolling_7_day_threshold_recovered_at: timestamp({ withTimezone: true, mode: 'string' }), + rolling_30_day_threshold_crossing_active: boolean().default(false).notNull(), + active_rolling_30_day_threshold_event_id: uuid().references(() => cost_insight_events.id, { + onDelete: 'set null', + }), + rolling_30_day_threshold_crossing_started_at: timestamp({ + withTimezone: true, + mode: 'string', + }), + rolling_30_day_threshold_reviewed_at: timestamp({ withTimezone: true, mode: 'string' }), + rolling_30_day_threshold_recovered_at: timestamp({ withTimezone: true, mode: 'string' }), created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), updated_at: timestamp({ withTimezone: true, mode: 'string' }) .defaultNow() @@ -3217,12 +3267,12 @@ export const cost_insight_owner_states = pgTable( index('IDX_cost_insight_owner_states_unreviewed_user') .on(table.owned_by_user_id, table.updated_at) .where( - sql`${table.owned_by_user_id} IS NOT NULL AND ((${table.active_anomaly_event_id} IS NOT NULL AND ${table.active_anomaly_reviewed_at} IS NULL) OR (${table.active_threshold_event_id} IS NOT NULL AND ${table.threshold_reviewed_at} IS NULL))` + sql`${table.owned_by_user_id} IS NOT NULL AND ((${table.active_anomaly_event_id} IS NOT NULL AND ${table.active_anomaly_reviewed_at} IS NULL) OR (${table.active_threshold_event_id} IS NOT NULL AND ${table.threshold_reviewed_at} IS NULL) OR (${table.active_rolling_7_day_threshold_event_id} IS NOT NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL) OR (${table.active_rolling_30_day_threshold_event_id} IS NOT NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL))` ), index('IDX_cost_insight_owner_states_unreviewed_org') .on(table.owned_by_organization_id, table.updated_at) .where( - sql`${table.owned_by_organization_id} IS NOT NULL AND ((${table.active_anomaly_event_id} IS NOT NULL AND ${table.active_anomaly_reviewed_at} IS NULL) OR (${table.active_threshold_event_id} IS NOT NULL AND ${table.threshold_reviewed_at} IS NULL))` + sql`${table.owned_by_organization_id} IS NOT NULL AND ((${table.active_anomaly_event_id} IS NOT NULL AND ${table.active_anomaly_reviewed_at} IS NULL) OR (${table.active_threshold_event_id} IS NOT NULL AND ${table.threshold_reviewed_at} IS NULL) OR (${table.active_rolling_7_day_threshold_event_id} IS NOT NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL) OR (${table.active_rolling_30_day_threshold_event_id} IS NOT NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL))` ), check( 'cost_insight_owner_states_owner_check', @@ -3236,6 +3286,14 @@ export const cost_insight_owner_states = pgTable( 'cost_insight_owner_states_threshold_active_check', sql`${table.threshold_crossing_active} = TRUE OR (${table.active_threshold_event_id} IS NULL AND ${table.threshold_crossing_started_at} IS NULL AND ${table.threshold_reviewed_at} IS NULL)` ), + check( + 'cost_insight_owner_states_7_day_threshold_active_check', + sql`${table.rolling_7_day_threshold_crossing_active} = TRUE OR (${table.active_rolling_7_day_threshold_event_id} IS NULL AND ${table.rolling_7_day_threshold_crossing_started_at} IS NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL)` + ), + check( + 'cost_insight_owner_states_30_day_threshold_active_check', + sql`${table.rolling_30_day_threshold_crossing_active} = TRUE OR (${table.active_rolling_30_day_threshold_event_id} IS NULL AND ${table.rolling_30_day_threshold_crossing_started_at} IS NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL)` + ), ] ); From b4b660f754d0dae7081969dd2d54adbba1e631dc Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Fri, 26 Jun 2026 21:08:19 +0200 Subject: [PATCH 10/11] fix(cost-insights): address PR review feedback --- .plans/cost-insights-data-layer.md | 10 +- .../stories/cost-insights/AskKilo.stories.tsx | 10 +- .../cost-insights/Overview.stories.tsx | 33 +- .../cost-insights/Settings.stories.tsx | 2 +- .../cost-insights/costInsightsFixtures.ts | 142 ++++--- .../app/(app)/components/SidebarMenuList.tsx | 6 + .../cron/cost-insights-hourly/route.test.ts | 73 ++++ .../api/cron/cost-insights-hourly/route.ts | 16 +- .../src/app/api/exa/[...path]/route.test.ts | 38 ++ apps/web/src/app/api/exa/[...path]/route.ts | 51 ++- .../CostInsightsOverviewClient.tsx | 41 +- .../CostInsightsSettingsClient.tsx | 8 +- .../ask-kilo/CostInsightsAskKiloView.tsx | 227 +++-------- .../cost-insights/formatting.test.ts | 6 +- .../components/cost-insights/formatting.ts | 16 +- .../cost-insights/overview/AskKiloInput.tsx | 81 ++-- .../overview/CostInsightsDashboardView.tsx | 7 +- .../overview/DashboardNotices.tsx | 16 +- .../overview/SpendEvidenceCard.tsx | 156 +++++--- .../settings/CostInsightsSettingsView.tsx | 128 +++--- .../cost-insights/shared/LocalDateTime.tsx | 7 +- .../web/src/components/cost-insights/types.ts | 23 +- .../evaluation.integration.test.ts | 196 ++++++++- apps/web/src/lib/cost-insights/evaluation.ts | 375 +++++++++++++----- apps/web/src/lib/cost-insights/jobs.ts | 40 +- .../notifications.integration.test.ts | 96 +++++ .../src/lib/cost-insights/notifications.ts | 61 ++- apps/web/src/lib/cost-insights/policy.test.ts | 24 ++ apps/web/src/lib/cost-insights/policy.ts | 17 +- .../src/lib/cost-insights/presenter.test.ts | 256 +++++++++++- apps/web/src/lib/cost-insights/presenter.ts | 211 +++++++--- apps/web/src/lib/cost-insights/repository.ts | 373 ++++++++++++++--- .../cost-insights/spend-evidence-seed.test.ts | 87 ++++ .../cost-insights/spend-repository.test.ts | 2 +- .../src/lib/cost-insights/spend-repository.ts | 91 +++-- .../lib/exa-usage-log-indexes-script.test.ts | 171 +++++++- apps/web/src/lib/exa-usage-partitions.test.ts | 18 + apps/web/src/lib/exa-usage-partitions.ts | 13 + .../src/routers/cost-insights-router.test.ts | 225 ++++++++++- apps/web/src/routers/cost-insights-router.ts | 161 +------- .../organization-cost-insights-router.test.ts | 51 +++ .../organization-cost-insights-router.ts | 5 +- .../src/scripts/db/exa-usage-log-indexes.ts | 95 ++++- dev/seed/cost-insights/spend-evidence.ts | 356 +++++++++++++++-- packages/db/src/cost-insights-rollups.ts | 39 +- packages/db/src/index.ts | 3 +- ...a_dare.sql => 0174_bizarre_piledriver.sql} | 40 +- .../db/src/migrations/meta/0174_snapshot.json | 252 +++++++++++- packages/db/src/migrations/meta/_journal.json | 4 +- packages/db/src/schema.test.ts | 2 +- packages/db/src/schema.ts | 78 +++- 51 files changed, 3481 insertions(+), 958 deletions(-) create mode 100644 apps/web/src/app/api/cron/cost-insights-hourly/route.test.ts create mode 100644 apps/web/src/lib/cost-insights/notifications.integration.test.ts create mode 100644 apps/web/src/lib/cost-insights/spend-evidence-seed.test.ts rename packages/db/src/migrations/{0174_sleepy_virginia_dare.sql => 0174_bizarre_piledriver.sql} (85%) diff --git a/.plans/cost-insights-data-layer.md b/.plans/cost-insights-data-layer.md index 2148766ae5..09e3c6920e 100644 --- a/.plans/cost-insights-data-layer.md +++ b/.plans/cost-insights-data-layer.md @@ -589,9 +589,15 @@ Do not attach prompts, auth headers, cookies, tokens, Exa request bodies, user e ## Operator and local seed notes -`apps/web/src/scripts/db/exa-usage-log-indexes.ts` creates two partial indexes per historical Exa leaf partition with `CREATE INDEX CONCURRENTLY IF NOT EXISTS`. Future partitions receive equivalent indexes during provisioning. Production runs should always set a small `--max-partitions`; the script does not currently verify `pg_index.indisvalid`/`indisready` after a failed concurrent build. +`apps/web/src/scripts/db/exa-usage-log-indexes.ts` creates two partial indexes per historical Exa leaf partition with `CREATE INDEX CONCURRENTLY IF NOT EXISTS`. Future partitions receive equivalent indexes during provisioning. Production runs should always set a small `--max-partitions`. Before and after each planned index, the operator checks the schema-qualified `pg_index.indisvalid` and `pg_index.indisready` state; an interrupted invalid index is dropped and rebuilt concurrently. -`dev/seed/cost-insights/spend-evidence.ts` is local-only and refuses production or non-loopback database targets. It seeds dedicated personal and organization owners using AI Gateway and KiloClaw canonical rows, then writes matching rollups through the production capture helper. It does not seed Exa or Coding Plan. It also extends global local coverage, so use it on a disposable/local database rather than a clone where unrelated canonical rows may lack rollups. Fixture users have placeholder Stripe IDs and should not be used on Stripe-backed pages. +`dev/seed/cost-insights/spend-evidence.ts` is local-only and refuses production or non-loopback database targets. It seeds dedicated personal and organization owners with canonical AI Gateway, Exa, Coding Plan, and KiloClaw records and matching owner rollups. Default `--coverage-mode preserve` never deletes or rewrites global v1 coverage. For full 1h (current UTC hour), 24h, 7d, 30d, and 90d UI evidence on a disposable local database, run: + +```sh +pnpm dev:seed cost-insights:spend-evidence --rollup-mode healthy --coverage-mode disposable-full +``` + +`disposable-full` verifies that the 90-day fixture range has no unrelated canonical records, owner rollups, or unresolved degraded intervals before replacing global v1 coverage, then verifies the written coverage state. It refuses databases containing unrelated evidence; use normal preserved coverage on shared or cloned databases. ## Verification completed diff --git a/apps/storybook/stories/cost-insights/AskKilo.stories.tsx b/apps/storybook/stories/cost-insights/AskKilo.stories.tsx index d2b5516a2e..9e8913b65c 100644 --- a/apps/storybook/stories/cost-insights/AskKilo.stories.tsx +++ b/apps/storybook/stories/cost-insights/AskKilo.stories.tsx @@ -11,14 +11,18 @@ const meta: Meta = { export default meta; type Story = StoryObj; -function AskKiloStory() { +function AskKiloStory({ initialQuestion }: { initialQuestion?: string }) { return ( - + ); } -export const Conversation: Story = { +export const DisabledPreview: Story = { render: () => , }; + +export const DisabledPreviewWithQuestion: Story = { + render: () => , +}; diff --git a/apps/storybook/stories/cost-insights/Overview.stories.tsx b/apps/storybook/stories/cost-insights/Overview.stories.tsx index 074c225a27..9b6b3fc968 100644 --- a/apps/storybook/stories/cost-insights/Overview.stories.tsx +++ b/apps/storybook/stories/cost-insights/Overview.stories.tsx @@ -31,24 +31,23 @@ const meta: Meta = { export default meta; type Story = StoryObj; +type OverviewStoryOptions = { + isLoading?: boolean; + isError?: boolean; + attention?: 'none' | 'alert'; + pendingSuggestionId?: string; +}; + function CostInsightsOverviewStory({ data, options = {}, initialPage = 'dashboard', }: { data: CostInsightsDashboardData; - options?: { isLoading?: boolean; isError?: boolean; attention?: 'none' | 'alert' }; + options?: OverviewStoryOptions; initialPage?: CostInsightsPage; }) { const [activePage, setActivePage] = useState(initialPage); - const [askKiloQuestion, setAskKiloQuestion] = useState( - 'Create a graph of my costs for the last week' - ); - - function handleAskKilo(question: string) { - setAskKiloQuestion(question); - setActivePage('ask'); - } const basePath = data.owner.type === 'organization' @@ -64,24 +63,21 @@ function CostInsightsOverviewStory({ onPageChange={setActivePage} > {activePage === 'ask' ? ( - + ) : ( )} ); } -function renderDashboard( - data: CostInsightsDashboardData, - options: { isLoading?: boolean; isError?: boolean; attention?: 'none' | 'alert' } = {} -) { +function renderDashboard(data: CostInsightsDashboardData, options: OverviewStoryOptions = {}) { return ; } @@ -128,6 +124,13 @@ export const CodingPlanSuggestion: Story = { render: () => renderDashboard(dashboardData({ suggestions: [codingPlanSuggestion] })), }; +export const SuggestionDismissPending: Story = { + render: () => + renderDashboard(dashboardData({ suggestions: [kiloPassSuggestion] }), { + pendingSuggestionId: kiloPassSuggestion.id, + }), +}; + export const AlertAndSuggestion: Story = { render: () => renderDashboard( diff --git a/apps/storybook/stories/cost-insights/Settings.stories.tsx b/apps/storybook/stories/cost-insights/Settings.stories.tsx index 61581365f8..4e92d26872 100644 --- a/apps/storybook/stories/cost-insights/Settings.stories.tsx +++ b/apps/storybook/stories/cost-insights/Settings.stories.tsx @@ -42,7 +42,7 @@ export const SevenDayThresholdOnly: Story = { ), }; -export const AlertsOffWithSavedThreshold: Story = { +export const AlertsOffWithSavedOptions: Story = { render: () => renderSettings( settingsData({ diff --git a/apps/storybook/stories/cost-insights/costInsightsFixtures.ts b/apps/storybook/stories/cost-insights/costInsightsFixtures.ts index d31b81253c..a45a58f49b 100644 --- a/apps/storybook/stories/cost-insights/costInsightsFixtures.ts +++ b/apps/storybook/stories/cost-insights/costInsightsFixtures.ts @@ -28,6 +28,35 @@ function money(value: number) { return (value >= 100 ? wholeDollarFormatter : currencyFormatter).format(value); } +type CompleteSpendEvidencePoint = Extract; +type CompleteSpendEvidenceInput = Pick< + CompleteSpendEvidencePoint, + 'label' | 'variableUsd' | 'scheduledUsd' | 'anomalyThresholdUsd' +>; + +function completeSpendEvidence( + points: CompleteSpendEvidenceInput[], + options: { start: string; periodHours: number } +): CompleteSpendEvidencePoint[] { + const startTime = new Date(options.start).getTime(); + return points.map((point, index) => { + const periodStart = new Date( + startTime + index * options.periodHours * 60 * 60 * 1000 + ).toISOString(); + const periodEndExclusive = new Date( + startTime + (index + 1) * options.periodHours * 60 * 60 * 1000 + ).toISOString(); + return { + ...point, + periodStart, + periodEndExclusive, + coveredHours: options.periodHours, + totalHours: options.periodHours, + coverage: 'complete', + }; + }); +} + function buildSpendMetrics({ currentHourUsd, baselineUsd, @@ -106,64 +135,58 @@ export const emptyMetrics: SpendMetric[] = buildSpendMetrics({ rolling24hUsd: 0, }); -export const evidence24h: SpendEvidencePoint[] = [ - { label: '00:00', variableUsd: 2.4, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '01:00', variableUsd: 3.1, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '02:00', variableUsd: 1.8, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '03:00', variableUsd: 0, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '04:00', variableUsd: 4.6, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '05:00', variableUsd: 6.2, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '06:00', variableUsd: 7.4, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '07:00', variableUsd: 8.1, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '08:00', variableUsd: 11.5, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '09:00', variableUsd: 13.2, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '10:00', variableUsd: 15.4, scheduledUsd: 12, anomalyThresholdUsd: 18 }, - { label: '11:00', variableUsd: 9.8, scheduledUsd: 0, anomalyThresholdUsd: 18 }, -]; - -export const evidenceThisHour: SpendEvidencePoint[] = [ - { label: '10:00', variableUsd: 112.7, scheduledUsd: 0, anomalyThresholdUsd: 18 }, -]; - -export const evidenceAnomaly: SpendEvidencePoint[] = [ - ...evidence24h.slice(0, 8), - { label: '08:00', variableUsd: 19.25, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '09:00', variableUsd: 42.8, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: '10:00', variableUsd: 74.35, scheduledUsd: 0, anomalyThresholdUsd: 18 }, - { label: 'Now', variableUsd: 112.7, scheduledUsd: 0, anomalyThresholdUsd: 18 }, -]; - -export const evidence7d: SpendEvidencePoint[] = [ - { label: 'Thu', variableUsd: 42, scheduledUsd: 12, anomalyThresholdUsd: 48 }, - { label: 'Fri', variableUsd: 36, scheduledUsd: 0, anomalyThresholdUsd: 48 }, - { label: 'Sat', variableUsd: 12, scheduledUsd: 0, anomalyThresholdUsd: 48 }, - { label: 'Sun', variableUsd: 9, scheduledUsd: 0, anomalyThresholdUsd: 48 }, - { label: 'Mon', variableUsd: 51, scheduledUsd: 0, anomalyThresholdUsd: 48 }, - { label: 'Tue', variableUsd: 63, scheduledUsd: 0, anomalyThresholdUsd: 48 }, - { label: 'Wed', variableUsd: 44, scheduledUsd: 24, anomalyThresholdUsd: 48 }, -]; - -export const evidence30d: SpendEvidencePoint[] = Array.from({ length: 30 }, (_, index) => ({ - label: `Jun ${index + 1}`, - variableUsd: 18 + ((index * 13) % 47), - scheduledUsd: index % 7 === 2 ? 12 : 0, -})); - -export const evidence90d: SpendEvidencePoint[] = [ - { label: 'Mar 30', variableUsd: 118, scheduledUsd: 12 }, - { label: 'Apr 6', variableUsd: 142, scheduledUsd: 24 }, - { label: 'Apr 13', variableUsd: 97, scheduledUsd: 0 }, - { label: 'Apr 20', variableUsd: 166, scheduledUsd: 12 }, - { label: 'Apr 27', variableUsd: 154, scheduledUsd: 24 }, - { label: 'May 4', variableUsd: 189, scheduledUsd: 0 }, - { label: 'May 11', variableUsd: 203, scheduledUsd: 12 }, - { label: 'May 18', variableUsd: 171, scheduledUsd: 24 }, - { label: 'May 25', variableUsd: 214, scheduledUsd: 0 }, - { label: 'Jun 1', variableUsd: 226, scheduledUsd: 12 }, - { label: 'Jun 8', variableUsd: 198, scheduledUsd: 24 }, - { label: 'Jun 15', variableUsd: 241, scheduledUsd: 0 }, - { label: 'Jun 22', variableUsd: 186, scheduledUsd: 12 }, -]; +export const evidence24h = completeSpendEvidence( + Array.from({ length: 24 }, (_, index) => ({ + label: `${index.toString().padStart(2, '0')}:00`, + variableUsd: index === 3 ? 0 : 2.4 + ((index * 17) % 31) / 2, + scheduledUsd: index === 10 ? 12 : 0, + anomalyThresholdUsd: 18, + })), + { start: '2026-06-25T10:00:00.000Z', periodHours: 1 } +); + +export const evidenceThisHour = completeSpendEvidence( + [{ label: 'Now', variableUsd: 112.7, scheduledUsd: 0, anomalyThresholdUsd: 18 }], + { start: '2026-06-26T09:00:00.000Z', periodHours: 1 } +); + +export const evidenceAnomaly = completeSpendEvidence( + Array.from({ length: 24 }, (_, index) => ({ + label: index === 23 ? 'Now' : `${index.toString().padStart(2, '0')}:00`, + variableUsd: index === 23 ? 112.7 : index === 22 ? 74.35 : 2.4 + ((index * 11) % 27), + scheduledUsd: 0, + anomalyThresholdUsd: 18, + })), + { start: '2026-06-25T10:00:00.000Z', periodHours: 1 } +); + +export const evidence7d = completeSpendEvidence( + Array.from({ length: 168 }, (_, index) => ({ + label: `Hour ${index + 1}`, + variableUsd: index % 19 === 0 ? 0 : 3 + ((index * 13) % 47), + scheduledUsd: index % 72 === 24 ? 12 : 0, + anomalyThresholdUsd: 48, + })), + { start: '2026-06-19T10:00:00.000Z', periodHours: 1 } +); + +export const evidence30d = completeSpendEvidence( + Array.from({ length: 30 }, (_, index) => ({ + label: `Day ${index + 1}`, + variableUsd: 18 + ((index * 13) % 47), + scheduledUsd: index % 7 === 2 ? 12 : 0, + })), + { start: '2026-05-27T10:00:00.000Z', periodHours: 24 } +); + +export const evidence90d = completeSpendEvidence( + Array.from({ length: 13 }, (_, index) => ({ + label: `Week ${index + 1}`, + variableUsd: 97 + ((index * 29) % 144), + scheduledUsd: index % 3 === 1 ? 24 : index % 3 === 0 ? 12 : 0, + })), + { start: '2026-03-28T10:00:00.000Z', periodHours: 168 } +); export const personalDrivers: SpendDriver[] = [ { @@ -306,6 +329,7 @@ export function spendDriversByRange( export const anomalyAlert = { type: 'anomaly', + eventId: '00000000-0000-4000-8000-000000000001', title: 'Spend is unusually high this hour', description: "Usage-based spend is well above this account's recent hourly pattern.", facts: [ @@ -327,6 +351,7 @@ export const anomalyAlert = { export const thresholdAlert = { type: 'threshold', + eventId: '00000000-0000-4000-8000-000000000002', title: '24-hour spend threshold crossed', description: 'Spend reached $184.90 against the $150.00 threshold.', facts: [ @@ -348,6 +373,7 @@ export const thresholdAlert = { export const threshold7DayAlert = { type: 'threshold_7d', + eventId: '00000000-0000-4000-8000-000000000003', title: '7-day spend threshold crossed', description: 'Spend reached $536.40 against the $500.00 threshold.', facts: [ diff --git a/apps/web/src/app/(app)/components/SidebarMenuList.tsx b/apps/web/src/app/(app)/components/SidebarMenuList.tsx index 3054d54371..555b9f3f3c 100644 --- a/apps/web/src/app/(app)/components/SidebarMenuList.tsx +++ b/apps/web/src/app/(app)/components/SidebarMenuList.tsx @@ -76,6 +76,11 @@ export default function SidebarMenuList({ {item.title} )} {item.suffixIcon && } + {isNumericBadge && item.badge && ( + + {item.badge} {item.badge === '1' ? 'item needs' : 'items need'} review + + )} ); const buttonClassName = cn( @@ -110,6 +115,7 @@ export default function SidebarMenuList({ )} {item.badge && ( ({ CRON_SECRET: 'cron-secret' })); +jest.mock('@/lib/cost-insights/jobs', () => ({ runCostInsightHourlySweep: jest.fn() })); +const mockSentryLog = jest.fn(); +jest.mock('@/lib/utils.server', () => ({ sentryLogger: jest.fn(() => mockSentryLog) })); + +import { runCostInsightHourlySweep } from '@/lib/cost-insights/jobs'; +import { GET } from './route'; + +const mockRunCostInsightHourlySweep = jest.mocked(runCostInsightHourlySweep); + +function summary(failedOwners: Array<{ owner: { type: 'user'; id: string }; error: string }> = []) { + return { + evaluatedOwners: 2, + failedOwners, + dirtyEvaluations: { + claimed: 1, + evaluatedOwners: [{ type: 'user' as const, id: 'user-1' }], + failedOwners: [], + }, + notifications: { + claimed: 1, + sent: 1, + skipped: 0, + terminalized: 0, + failed: 0, + }, + }; +} + +describe('GET /api/cron/cost-insights-hourly', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns failure status and telemetry for a partial owner failure', async () => { + mockRunCostInsightHourlySweep.mockResolvedValue( + summary([{ owner: { type: 'user', id: 'user-2' }, error: 'evaluation failed' }]) + ); + + const response = await GET( + new NextRequest('http://localhost:3000/api/cron/cost-insights-hourly', { + headers: { authorization: 'Bearer cron-secret' }, + }) + ); + const body = await response.json(); + + expect(response.status).toBe(500); + expect(body).toMatchObject({ success: false, partialFailure: true }); + expect(mockSentryLog).toHaveBeenCalledWith( + 'Cost Insights hourly sweep completed with partial failures', + expect.objectContaining({ failedOwnerCount: 1 }) + ); + }); + + test('returns success only when all work succeeds', async () => { + mockRunCostInsightHourlySweep.mockResolvedValue(summary()); + + const response = await GET( + new NextRequest('http://localhost:3000/api/cron/cost-insights-hourly', { + headers: { authorization: 'Bearer cron-secret' }, + }) + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + success: true, + partialFailure: false, + }); + expect(mockSentryLog).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/app/api/cron/cost-insights-hourly/route.ts b/apps/web/src/app/api/cron/cost-insights-hourly/route.ts index aeea7bbd6b..002d685006 100644 --- a/apps/web/src/app/api/cron/cost-insights-hourly/route.ts +++ b/apps/web/src/app/api/cron/cost-insights-hourly/route.ts @@ -24,13 +24,25 @@ export async function GET(request: Request) { } const summary = await runCostInsightHourlySweep(db); + const hasFailures = + summary.failedOwners.length > 0 || + summary.notifications.failed > 0 || + summary.notifications.terminalized > 0; + if (hasFailures) { + sentryLogger('cron', 'error')('Cost Insights hourly sweep completed with partial failures', { + failedOwnerCount: summary.failedOwners.length, + failedNotificationCount: summary.notifications.failed, + terminalizedNotificationCount: summary.notifications.terminalized, + }); + } return NextResponse.json( { - success: true, + success: !hasFailures, + partialFailure: hasFailures, summary, timestamp: new Date().toISOString(), }, - { status: 200 } + { status: hasFailures ? 500 : 200 } ); } diff --git a/apps/web/src/app/api/exa/[...path]/route.test.ts b/apps/web/src/app/api/exa/[...path]/route.test.ts index fa3ed44488..5c8bcb0dc7 100644 --- a/apps/web/src/app/api/exa/[...path]/route.test.ts +++ b/apps/web/src/app/api/exa/[...path]/route.test.ts @@ -10,6 +10,7 @@ import { } from '@/lib/exa-usage'; import { EXA_MONTHLY_ALLOWANCE_MICRODOLLARS } from '@/lib/constants'; import { getBalanceAndOrgSettings } from '@/lib/organizations/organization-usage'; +import { captureException } from '@sentry/nextjs'; // Capture promises scheduled via next/server `after` so tests can await them. let afterCallbacks: (() => Promise)[] = []; @@ -37,12 +38,14 @@ jest.mock('@/lib/config.server', () => ({ jest.mock('@/lib/user/server'); jest.mock('@/lib/exa-usage'); jest.mock('@/lib/organizations/organization-usage'); +jest.mock('@sentry/nextjs', () => ({ captureException: jest.fn() })); const mockedGetUserFromAuth = jest.mocked(getUserFromAuth); const mockedGetExaMonthlyUsage = jest.mocked(getExaMonthlyUsage); const mockedGetExaFreeAllowanceMicrodollars = jest.mocked(getExaFreeAllowanceMicrodollars); const mockedRecordExaUsage = jest.mocked(recordExaUsage); const mockedGetBalanceAndOrgSettings = jest.mocked(getBalanceAndOrgSettings); +const mockedCaptureException = jest.mocked(captureException); const mockedFetch = jest.fn() as jest.MockedFunction; const originalFetch = globalThis.fetch; @@ -399,6 +402,7 @@ describe('POST /api/exa/[...path]', () => { await flushAfterCallbacks(); expect(mockedRecordExaUsage).not.toHaveBeenCalled(); + expect(mockedCaptureException).not.toHaveBeenCalled(); }); it('does not record cost when costDollars is zero', async () => { @@ -412,6 +416,40 @@ describe('POST /api/exa/[...path]', () => { await flushAfterCallbacks(); expect(mockedRecordExaUsage).not.toHaveBeenCalled(); + expect(mockedCaptureException).not.toHaveBeenCalled(); + }); + + it.each([ + ['nonnumeric', '0.007'], + ['negative', -0.007], + ['too small to represent in microdollars', 0.0000001], + ['too large to represent safely in microdollars', 10_000_000_000], + ])('captures and ignores %s costDollars totals', async (_description, total) => { + setUserAuth(); + mockedFetch.mockResolvedValue(makeUpstreamResponse({ results: [], costDollars: { total } })); + + const { POST } = await import('./route'); + await POST(makeRequest('/search') as never); + await expect(flushAfterCallbacks()).resolves.toBeUndefined(); + + expect(mockedRecordExaUsage).not.toHaveBeenCalled(); + expect(mockedCaptureException).toHaveBeenCalledTimes(1); + }); + + it('captures and ignores non-finite costDollars totals', async () => { + setUserAuth(); + mockedFetch.mockResolvedValue( + new Response('{"results":[],"costDollars":{"total":1e400}}', { + headers: { 'content-type': 'application/json' }, + }) + ); + + const { POST } = await import('./route'); + await POST(makeRequest('/search') as never); + await expect(flushAfterCallbacks()).resolves.toBeUndefined(); + + expect(mockedRecordExaUsage).not.toHaveBeenCalled(); + expect(mockedCaptureException).toHaveBeenCalledTimes(1); }); it('passes featureId from header and type from body to recordExaUsage', async () => { diff --git a/apps/web/src/app/api/exa/[...path]/route.ts b/apps/web/src/app/api/exa/[...path]/route.ts index 3c88bcbbf2..729510b68e 100644 --- a/apps/web/src/app/api/exa/[...path]/route.ts +++ b/apps/web/src/app/api/exa/[...path]/route.ts @@ -14,8 +14,17 @@ import { readDb } from '@/lib/drizzle'; import { captureException } from '@sentry/nextjs'; import { validateFeatureHeader, FEATURE_HEADER } from '@/lib/feature-detection'; import { EXA_ALLOWED_PATHS, isExaAllowedPath } from '@/lib/exa-paths'; +import { z } from 'zod'; const EXA_BASE_URL = 'https://api.exa.ai'; +const MICRODOLLARS_PER_DOLLAR = 1_000_000; +const ExaCostResponseSchema = z.object({ + costDollars: z + .object({ + total: z.number().finite().optional(), + }) + .optional(), +}); function extractExaPath(url: URL): string | null { const prefix = '/api/exa'; @@ -24,9 +33,18 @@ function extractExaPath(url: URL): string | null { return isExaAllowedPath(path) ? path : null; } -function extractCostDollars(responseBody: unknown): number | undefined { - const body = responseBody as { costDollars?: { total?: number } } | null; - return body?.costDollars?.total; +function extractCostMicrodollars(responseBody: unknown): number | undefined { + const costDollars = ExaCostResponseSchema.parse(responseBody).costDollars?.total; + if (costDollars === undefined || costDollars === 0) return undefined; + if (costDollars < 0) { + throw new Error('Exa response costDollars.total must be positive.'); + } + + const costMicrodollars = Math.round(costDollars * MICRODOLLARS_PER_DOLLAR); + if (!Number.isSafeInteger(costMicrodollars) || costMicrodollars <= 0) { + throw new Error('Exa response cost must convert to a positive safe integer.'); + } + return costMicrodollars; } export async function POST(request: NextRequest) { @@ -108,20 +126,19 @@ export async function POST(request: NextRequest) { try { const body: unknown = await cloned.json(); - const costDollars = extractCostDollars(body); - if (costDollars !== undefined && costDollars > 0) { - const costMicrodollars = Math.round(costDollars * 1_000_000); - await recordExaUsage({ - userId: user.id, - organizationId, - path: exaPath, - costMicrodollars, - chargedToBalance: isPaidRequest, - freeAllowanceMicrodollars: allowance, - featureId, - type, - }); - } + const costMicrodollars = extractCostMicrodollars(body); + if (costMicrodollars === undefined) return; + + await recordExaUsage({ + userId: user.id, + organizationId, + path: exaPath, + costMicrodollars, + chargedToBalance: isPaidRequest, + freeAllowanceMicrodollars: allowance, + featureId, + type, + }); } catch (error) { captureException(error, { tags: { diff --git a/apps/web/src/components/cost-insights/CostInsightsOverviewClient.tsx b/apps/web/src/components/cost-insights/CostInsightsOverviewClient.tsx index b1e6d7bd5b..7f3b38ba64 100644 --- a/apps/web/src/components/cost-insights/CostInsightsOverviewClient.tsx +++ b/apps/web/src/components/cost-insights/CostInsightsOverviewClient.tsx @@ -101,36 +101,36 @@ export function CostInsightsOverviewClient({ const personalAcknowledgeMutation = useMutation( trpc.costInsights.acknowledgeAlert.mutationOptions({ - onSuccess: () => { + onSuccess: async () => { + await invalidateCostInsights(); toast.success('Alert marked reviewed'); - void invalidateCostInsights(); }, onError: error => toast.error(error.message || 'Could not mark alert reviewed'), }) ); const organizationAcknowledgeMutation = useMutation( trpc.organizations.costInsights.acknowledgeAlert.mutationOptions({ - onSuccess: () => { + onSuccess: async () => { + await invalidateCostInsights(); toast.success('Alert marked reviewed'); - void invalidateCostInsights(); }, onError: error => toast.error(error.message || 'Could not mark alert reviewed'), }) ); const personalDismissMutation = useMutation( trpc.costInsights.dismissSuggestion.mutationOptions({ - onSuccess: () => { + onSuccess: async () => { + await invalidateCostInsights(); toast.success('Suggestion dismissed'); - void invalidateCostInsights(); }, onError: error => toast.error(error.message || 'Could not dismiss suggestion'), }) ); const organizationDismissMutation = useMutation( trpc.organizations.costInsights.dismissSuggestion.mutationOptions({ - onSuccess: () => { + onSuccess: async () => { + await invalidateCostInsights(); toast.success('Suggestion dismissed'); - void invalidateCostInsights(); }, onError: error => toast.error(error.message || 'Could not dismiss suggestion'), }) @@ -138,14 +138,15 @@ export function CostInsightsOverviewClient({ const handleAlertAction = (alert: DashboardAlert, action: DashboardAlertAction) => { if (action === 'acknowledge') { + const acknowledgement = { alertKind: alert.type, eventId: alert.eventId }; if (organizationId) { organizationAcknowledgeMutation.mutate({ organizationId, - alertKind: alert.type, + ...acknowledgement, }); return; } - personalAcknowledgeMutation.mutate({ alertKind: alert.type }); + personalAcknowledgeMutation.mutate(acknowledgement); return; } @@ -165,7 +166,15 @@ export function CostInsightsOverviewClient({ router.push(`${basePath}/config`); }; + const activeDismissMutation = organizationId + ? organizationDismissMutation + : personalDismissMutation; + const pendingSuggestionId = activeDismissMutation.isPending + ? activeDismissMutation.variables?.suggestionId + : undefined; + const handleSuggestionDismiss = (suggestionId: string) => { + if (pendingSuggestionId === suggestionId) return; if (organizationId) { organizationDismissMutation.mutate({ organizationId, suggestionId }); return; @@ -177,16 +186,6 @@ export function CostInsightsOverviewClient({ trackSuggestionCta({ suggestionKind: suggestion.type }); }; - const handleAskKilo = (question: string) => { - trackUiInteraction({ - interaction: 'ask_kilo_question_submitted', - source: 'dashboard', - experience: 'ui_only', - }); - const searchParams = new URLSearchParams({ question }); - router.push(`${basePath}/ask-kilo?${searchParams.toString()}`); - }; - const handleSpendRangeChange = (range: SpendRange) => { trackUiInteraction({ interaction: 'spend_range_selected', range }); }; @@ -206,6 +205,7 @@ export function CostInsightsOverviewClient({ isError={dashboardError} activityHref={`${basePath}/activity`} alertActionsDisabled={alertActionsDisabled} + pendingSuggestionId={pendingSuggestionId} onRetry={() => { void (organizationId ? refetchOrganizationDashboard() : refetchPersonalDashboard()); }} @@ -218,7 +218,6 @@ export function CostInsightsOverviewClient({ onSpendRangeChange={handleSpendRangeChange} onSuggestionCta={handleSuggestionCta} onSuggestionDismiss={handleSuggestionDismiss} - onAskKilo={handleAskKilo} /> ); } diff --git a/apps/web/src/components/cost-insights/CostInsightsSettingsClient.tsx b/apps/web/src/components/cost-insights/CostInsightsSettingsClient.tsx index d5356c8300..e0e8dc9cbb 100644 --- a/apps/web/src/components/cost-insights/CostInsightsSettingsClient.tsx +++ b/apps/web/src/components/cost-insights/CostInsightsSettingsClient.tsx @@ -124,9 +124,9 @@ export function CostInsightsSettingsClient({ organizationId }: CostInsightsSetti isError: personalUpdateError, } = useMutation( trpc.costInsights.updateSettings.mutationOptions({ - onSuccess: () => { + onSuccess: async () => { + await invalidateCostInsights(); toast.success('Cost Insights settings saved'); - void invalidateCostInsights(); }, onError: error => toast.error(error.message || 'Could not save Cost Insights settings'), }) @@ -137,9 +137,9 @@ export function CostInsightsSettingsClient({ organizationId }: CostInsightsSetti isError: organizationUpdateError, } = useMutation( trpc.organizations.costInsights.updateSettings.mutationOptions({ - onSuccess: () => { + onSuccess: async () => { + await invalidateCostInsights(); toast.success('Cost Insights settings saved'); - void invalidateCostInsights(); }, onError: error => toast.error(error.message || 'Could not save Cost Insights settings'), }) diff --git a/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx b/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx index 22087360f6..b0198ed5b6 100644 --- a/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx +++ b/apps/web/src/components/cost-insights/ask-kilo/CostInsightsAskKiloView.tsx @@ -1,30 +1,15 @@ 'use client'; -import { useEffect, useRef, useState, type FormEvent } from 'react'; -import { BarChart3, ChevronDown, ChevronUp, Send } from 'lucide-react'; +import { useEffect, useRef } from 'react'; +import { Eye, Lock, Send } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useCostInsightsTracking } from '../useCostInsightsTracking'; -const askKiloChartData = [ - { date: 'Jun 18', cost: 1.42, color: 'var(--chart-1)' }, - { date: 'Jun 19', cost: 0.28, color: 'var(--chart-2)' }, - { date: 'Jun 20', cost: 0.17, color: 'var(--chart-3)' }, - { date: 'Jun 21', cost: 0, color: 'var(--chart-4)' }, - { date: 'Jun 22', cost: 0, color: 'var(--chart-5)' }, - { date: 'Jun 23', cost: 0.45, color: 'var(--chart-1)' }, - { date: 'Jun 24', cost: 0.31, color: 'var(--chart-2)' }, -]; - -type AskKiloMessage = { - id: string; - question: string; -}; - export function CostInsightsAskKiloView({ - initialQuestion = 'Create a graph of my costs for the last week', + initialQuestion, organizationId, }: { initialQuestion?: string; @@ -32,12 +17,7 @@ export function CostInsightsAskKiloView({ }) { const { trackUiInteraction } = useCostInsightsTracking(organizationId); const trackedAskKiloOwner = useRef(undefined); - const initialMessage = initialQuestion.trim() || 'Create a graph of my costs for the last week'; - const [question, setQuestion] = useState(''); - const [messages, setMessages] = useState([ - { id: 'initial', question: initialMessage }, - ]); - const [chartExpanded, setChartExpanded] = useState(true); + const previewQuestion = initialQuestion?.trim() ?? ''; useEffect(() => { const ownerKey = organizationId ?? 'personal'; @@ -46,159 +26,60 @@ export function CostInsightsAskKiloView({ trackUiInteraction({ interaction: 'ask_kilo_viewed' }); }, [organizationId, trackUiInteraction]); - function handleSubmit(event: FormEvent) { - event.preventDefault(); - const trimmedQuestion = question.trim(); - if (!trimmedQuestion) return; - - trackUiInteraction({ - interaction: 'ask_kilo_question_submitted', - source: 'follow_up', - experience: 'ui_only', - }); - setMessages(currentMessages => [ - ...currentMessages, - { id: `question-${currentMessages.length}`, question: trimmedQuestion }, - ]); - setQuestion(''); - } - return ( -
-
- {messages.map(message => ( -
-
- {message.question} -
-
-
- - {chartExpanded && ( -
-

- Model usage, Cost, Jun 18, 2026 to Jun 24, 2026 -

-

Cost by date

-
-
- Daily cost from June 18 to June 24. Peak cost was $1.42 on June 18. No spend - occurred June 21 or June 22. -
-
-
- {[1.6, 1.2, 0.8, 0.4, 0].map(value => ( - ${value.toFixed(2)} - ))} -
-
- - {askKiloChartData.map(item => ( -
-
- - {item.date.replace('Jun ', '')} - -
- ))} -
-
-
- Jun 18-24 -
-
-
- )} -
- -
-

Here is your daily cost trend for the last 7 days (Jun 18-24):

-
    -
  • - Total spend: $2.63 over the week -
  • -
  • - Daily average: $0.38 -
  • -
  • - Peak day: - Jun 18 at $1.42, 54% of the week's cost -
  • -
  • - Quietest days: Jun 21-22 with no - Credit spend -
  • -
  • - Trend: Spend peaked at the start of - the week, paused midweek, then resumed at a lower level. -
  • -
-

- The Jun 18 spike is the main driver of weekly spend. Break down that day by model - to identify which usage drove the cost. -

-
-
+
+
+
+
+
+ + +
+

+ Ask Kilo is not available yet +

+

+ This preview does not query spend data. No question is submitted and no financial + analysis is generated. When Ask Kilo becomes available, answers will use current Cost + Insights data for this Spend owner. +

- ))} -
+
- - -
- setQuestion(event.target.value)} - placeholder="Ask a follow-up about your spending" - className="bg-card h-12! rounded-xl pr-14 shadow-lg" - /> - +
+ +
+ + +
+

+ Input is disabled in this preview. +

- +
); } diff --git a/apps/web/src/components/cost-insights/formatting.test.ts b/apps/web/src/components/cost-insights/formatting.test.ts index 636aa8b3e7..1b6de3cb2f 100644 --- a/apps/web/src/components/cost-insights/formatting.test.ts +++ b/apps/web/src/components/cost-insights/formatting.test.ts @@ -42,13 +42,17 @@ describe('Cost Insights formatting', () => { expect(label).not.toContain('UTC'); }); - it('formats hourly evidence in the requested time zone', () => { + it('formats evidence labels in the requested time zone', () => { expect(formatSpendEvidenceTime('2026-06-26T08:00:00.000Z', '24h', 'America/New_York')).toBe( '04' ); expect(formatSpendEvidenceTime('2026-06-26T08:00:00.000Z', '7d', 'America/New_York')).toBe( 'Jun 26, 04' ); + expect(formatSpendEvidenceTime('2026-06-26T00:00:00.000Z', '30d', 'America/Los_Angeles')).toBe( + 'Jun 25' + ); + expect(formatSpendEvidenceTime('2026-06-25T18:00:00.000Z', '90d', 'Asia/Tokyo')).toBe('Jun 26'); }); it('formats elapsed windows with both local dates and times', () => { diff --git a/apps/web/src/components/cost-insights/formatting.ts b/apps/web/src/components/cost-insights/formatting.ts index f73cdc6b7e..293400d423 100644 --- a/apps/web/src/components/cost-insights/formatting.ts +++ b/apps/web/src/components/cost-insights/formatting.ts @@ -46,14 +46,16 @@ export function formatCostInsightDateTime(timestamp: string, timeZone?: string) }).format(new Date(timestamp)); } -export function formatSpendEvidenceTime( - timestamp: string, - range: '1h' | '24h' | '7d', - timeZone?: string -) { +export function formatSpendEvidenceTime(timestamp: string, range: SpendRange, timeZone?: string) { + const dateFields = + range === '30d' || range === '90d' + ? { month: 'short' as const, day: 'numeric' as const } + : range === '7d' + ? { month: 'short' as const, day: 'numeric' as const, hour: '2-digit' as const } + : { hour: '2-digit' as const }; + return new Intl.DateTimeFormat('en-US', { - ...(range === '7d' ? { month: 'short', day: 'numeric' } : {}), - hour: '2-digit', + ...dateFields, hourCycle: 'h23', timeZone, }).format(new Date(timestamp)); diff --git a/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx b/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx index 97c572909d..2ede1a14a8 100644 --- a/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx +++ b/apps/web/src/components/cost-insights/overview/AskKiloInput.tsx @@ -1,52 +1,53 @@ 'use client'; -import { useState, type FormEvent } from 'react'; -import { Send } from 'lucide-react'; +import { Eye, Send } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import type { CostInsightsOwner } from '../types'; -export function AskKiloInput({ - owner, - onSubmit, -}: { - owner: CostInsightsOwner; - onSubmit?: (question: string) => void; -}) { - const [question, setQuestion] = useState(''); - - function handleSubmit(event: FormEvent) { - event.preventDefault(); - const trimmedQuestion = question.trim(); - if (!trimmedQuestion) return; - - onSubmit?.(trimmedQuestion); - setQuestion(''); - } +export function AskKiloInput({ owner }: { owner: CostInsightsOwner }) { + const helpId = 'ask-kilo-question-help'; return ( -
- - setQuestion(event.target.value)} - placeholder="Ask Kilo about your spending" - className="bg-card h-12! rounded-xl pr-14" - /> - -
+
+
+ + +

+ Ask Kilo is not available yet. No question is submitted and no analysis is generated. +

+
+
+ + + + Disabled preview. Ask Kilo cannot analyze spend data in this version. + + +
+
); } diff --git a/apps/web/src/components/cost-insights/overview/CostInsightsDashboardView.tsx b/apps/web/src/components/cost-insights/overview/CostInsightsDashboardView.tsx index f37079a2fe..3b6cb9794f 100644 --- a/apps/web/src/components/cost-insights/overview/CostInsightsDashboardView.tsx +++ b/apps/web/src/components/cost-insights/overview/CostInsightsDashboardView.tsx @@ -43,6 +43,7 @@ export function CostInsightsDashboardView({ isError = false, activityHref, alertActionsDisabled = false, + pendingSuggestionId, onRetry, onSetupAlerts, onAlertAction, @@ -50,13 +51,13 @@ export function CostInsightsDashboardView({ onSpendRangeChange, onSuggestionCta, onSuggestionDismiss, - onAskKilo, }: { data?: CostInsightsDashboardData; isLoading?: boolean; isError?: boolean; activityHref?: string; alertActionsDisabled?: boolean; + pendingSuggestionId?: string; onRetry?: () => void; onSetupAlerts?: () => void; onAlertAction?: ( @@ -67,7 +68,6 @@ export function CostInsightsDashboardView({ onSpendRangeChange?: (range: SpendRange) => void; onSuggestionCta?: (suggestion: CostInsightsDashboardData['suggestions'][number]) => void; onSuggestionDismiss?: (suggestionId: string) => void; - onAskKilo?: (question: string) => void; }) { const [selectedRange, setSelectedRange] = useState(); @@ -92,7 +92,7 @@ export function CostInsightsDashboardView({ return (
- + {data.alerts.map(alert => ( onSuggestionCta?.(suggestion)} onDismiss={() => onSuggestionDismiss?.(suggestion.id)} /> diff --git a/apps/web/src/components/cost-insights/overview/DashboardNotices.tsx b/apps/web/src/components/cost-insights/overview/DashboardNotices.tsx index 2bf9cc70a8..522e06df8e 100644 --- a/apps/web/src/components/cost-insights/overview/DashboardNotices.tsx +++ b/apps/web/src/components/cost-insights/overview/DashboardNotices.tsx @@ -8,6 +8,7 @@ import { ChevronDown, ChevronUp, Lightbulb, + Loader2, TrendingUp, XCircle, } from 'lucide-react'; @@ -307,11 +308,13 @@ function AlertDriverEvidencePanel({ export function SuggestionCard({ suggestion, canManage = true, + dismissPending = false, onCta, onDismiss, }: { suggestion: CostSuggestion; canManage?: boolean; + dismissPending?: boolean; onCta?: () => void; onDismiss?: () => void; }) { @@ -356,10 +359,19 @@ export function SuggestionCard({ type="button" variant="outline" className="min-h-control-touch w-full sm:min-h-0" + disabled={dismissPending} + aria-busy={dismissPending} onClick={onDismiss} > -
)} diff --git a/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx b/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx index ac3f9ef70c..c3a72066df 100644 --- a/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx +++ b/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { CSSProperties } from 'react'; +import { useId, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react'; import { ArrowRight, Clock3 } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; @@ -18,13 +18,15 @@ export function SpendEvidenceCard({ range: SpendRange; }) { const viewerTimeZone = useViewerTimeZone(); + const chartInstructionsId = useId(); + const [activeBarIndex, setActiveBarIndex] = useState(0); + const barRefs = useRef>([]); const rawEvidence = range === data.range ? data.evidence : data.evidenceByRange[range]; const evidence = rawEvidence.map(point => ({ ...point, - label: - point.periodStart && (range === '1h' || range === '24h' || range === '7d') - ? formatSpendEvidenceTime(point.periodStart, range, viewerTimeZone) - : point.label, + label: formatSpendEvidenceTime(point.periodStart, range, viewerTimeZone), + variableUsd: point.variableUsd ?? 0, + scheduledUsd: point.scheduledUsd ?? 0, })); const totals = evidence.map(point => point.variableUsd + point.scheduledUsd); const maxSpend = Math.max(1, ...totals); @@ -35,13 +37,40 @@ export function SpendEvidenceCard({ '30d': 'Last 30 days', '90d': 'Last 90 days', }[range]; - const highestIndex = totals.indexOf(Math.max(...totals)); - const highest = evidence[highestIndex]; - const total = totals.reduce((sum, value) => sum + value, 0); + const highest = evidence + .filter(point => point.coverage === 'complete') + .reduce<(typeof evidence)[number] | undefined>((currentHighest, point) => { + if (!currentHighest) return point; + const currentTotal = currentHighest.variableUsd + currentHighest.scheduledUsd; + return point.variableUsd + point.scheduledUsd > currentTotal ? point : currentHighest; + }, undefined); + const hasIncompleteEvidence = evidence.some(point => point.coverage !== 'complete'); + const completeTotal = hasIncompleteEvidence + ? null + : totals.reduce((sum, value) => sum + value, 0); const isDenseRange = range === '30d'; const barMinimumWidth = range === '1h' ? '8rem' : range === '30d' ? '0.75rem' : range === '90d' ? '1.5rem' : '2rem'; const chartMinimumWidth = range === '1h' ? '12rem' : range === '30d' ? '40rem' : '32rem'; + const focusedBarIndex = Math.min(activeBarIndex, Math.max(0, evidence.length - 1)); + + const handleBarKeyDown = (event: KeyboardEvent, index: number) => { + let nextIndex: number | undefined; + if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { + nextIndex = (index + 1) % evidence.length; + } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { + nextIndex = (index - 1 + evidence.length) % evidence.length; + } else if (event.key === 'Home') { + nextIndex = 0; + } else if (event.key === 'End') { + nextIndex = evidence.length - 1; + } + if (nextIndex === undefined) return; + + event.preventDefault(); + setActiveBarIndex(nextIndex); + barRefs.current[nextIndex]?.focus(); + }; return ( @@ -67,13 +96,20 @@ export function SpendEvidenceCard({

- {rangeLabel}: {money(total)} total.{' '} + {completeTotal === null + ? `${rangeLabel}: spend total unavailable because one or more periods have incomplete coverage.` + : `${rangeLabel}: ${money(completeTotal)} total.`}{' '} {highest - ? `Highest period was ${highest.label} at ${money(highest.variableUsd + highest.scheduledUsd)}.` + ? `Highest complete period was ${highest.label} at ${money(highest.variableUsd + highest.scheduledUsd)}.` : ''}

+

+ Use Left and Right Arrow keys to inspect each period. Use Home and End to jump to the + first or last period. +

-
+ {rangeLabel} spend by period +
{range !== '1h' && (
diff --git a/apps/web/src/components/cost-insights/settings/CostInsightsSettingsView.tsx b/apps/web/src/components/cost-insights/settings/CostInsightsSettingsView.tsx index 629fb4a51a..9062afd2f5 100644 --- a/apps/web/src/components/cost-insights/settings/CostInsightsSettingsView.tsx +++ b/apps/web/src/components/cost-insights/settings/CostInsightsSettingsView.tsx @@ -108,71 +108,75 @@ export function CostInsightsSettingsView({
- {data.enabled && ( -
-
-
-

- Spend anomalies -

-

- Compare current-hour usage-based spend with your recent hourly pattern. -

-
-
- - - onChange?.({ anomalyAlertsEnabled })} - /> -
-
+
+ {!data.enabled && ( +

+ Spend Alerts are off. Saved anomaly and threshold settings apply when you turn + Spend Alerts on again. +

+ )} +
+
+

+ Spend anomalies +

+

+ Compare current-hour usage-based spend with your recent hourly pattern. +

+
+
+ + + onChange?.({ anomalyAlertsEnabled })} + /> +
+
- onChange?.({ thresholdUsd })} - /> + onChange?.({ thresholdUsd })} + /> - onChange?.({ threshold7DayUsd })} - /> + onChange?.({ threshold7DayUsd })} + /> - onChange?.({ threshold30DayUsd })} - /> -
- )} + onChange?.({ threshold30DayUsd })} + /> +
diff --git a/apps/web/src/components/cost-insights/shared/LocalDateTime.tsx b/apps/web/src/components/cost-insights/shared/LocalDateTime.tsx index ed8f484f16..44a0c30bcb 100644 --- a/apps/web/src/components/cost-insights/shared/LocalDateTime.tsx +++ b/apps/web/src/components/cost-insights/shared/LocalDateTime.tsx @@ -6,13 +6,12 @@ import { formatCostInsightDateTime } from '../formatting'; const subscribe = () => () => {}; export function useViewerTimeZone() { - return useSyncExternalStore( + const useClientTimeZone = useSyncExternalStore( subscribe, () => true, () => false - ) - ? undefined - : 'UTC'; + ); + return useClientTimeZone ? undefined : 'UTC'; } export function LocalDateTime({ diff --git a/apps/web/src/components/cost-insights/types.ts b/apps/web/src/components/cost-insights/types.ts index f5a97a83d8..a82ede5b98 100644 --- a/apps/web/src/components/cost-insights/types.ts +++ b/apps/web/src/components/cost-insights/types.ts @@ -19,14 +19,27 @@ export type SpendMetric = { icon: LucideIcon | SpendMetricIcon; }; -export type SpendEvidencePoint = { +type SpendEvidencePointBase = { label: string; - periodStart?: string; - variableUsd: number; - scheduledUsd: number; + periodStart: string; + periodEndExclusive: string; + coveredHours: number; + totalHours: number; anomalyThresholdUsd?: number; }; +export type SpendEvidencePoint = + | (SpendEvidencePointBase & { + coverage: 'complete'; + variableUsd: number; + scheduledUsd: number; + }) + | (SpendEvidencePointBase & { + coverage: 'partial' | 'unavailable'; + variableUsd: null; + scheduledUsd: null; + }); + export type SpendDriver = { id: string; label: string; @@ -54,6 +67,7 @@ export type AlertDriverEvidence = { export type DashboardAlert = | { type: 'anomaly'; + eventId: string; title: string; description: string; facts?: AlertFact[]; @@ -62,6 +76,7 @@ export type DashboardAlert = } | { type: 'threshold' | 'threshold_7d' | 'threshold_30d'; + eventId: string; title: string; description: string; facts?: AlertFact[]; diff --git a/apps/web/src/lib/cost-insights/evaluation.integration.test.ts b/apps/web/src/lib/cost-insights/evaluation.integration.test.ts index 4ab557f89b..23647920a9 100644 --- a/apps/web/src/lib/cost-insights/evaluation.integration.test.ts +++ b/apps/web/src/lib/cost-insights/evaluation.integration.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, test } from '@jest/globals'; import { captureCostInsightSpend } from '@kilocode/db/cost-insights-rollups'; import { + cost_insight_evaluation_dirty_owners, cost_insight_events, cost_insight_owner_configs, cost_insight_owner_hour_driver_buckets, @@ -14,7 +15,7 @@ import { and, eq } from 'drizzle-orm'; import { db } from '@/lib/drizzle'; import { addHours, floorUtcHour } from './policy'; import { getOwnerRollingDriverEvidenceExact, getOwnerRollingSpendExact } from './spend-repository'; -import { evaluateCostInsightsForOwner } from './evaluation'; +import { evaluateCostInsightsForOwner, processPendingCostInsightEvaluations } from './evaluation'; const testUserIds = new Set(); @@ -34,6 +35,9 @@ async function createUser(): Promise { afterEach(async () => { for (const userId of testUserIds) { await db.delete(microdollar_usage).where(eq(microdollar_usage.kilo_user_id, userId)); + await db + .delete(cost_insight_evaluation_dirty_owners) + .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)); await db .delete(cost_insight_owner_states) .where(eq(cost_insight_owner_states.owned_by_user_id, userId)); @@ -261,6 +265,20 @@ describe('Cost Insights evaluation integration', () => { spend_alerts_enabled: true, cost_suggestions_enabled: false, }); + await db.insert(microdollar_usage).values({ + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 30_000_000, + input_tokens: 1, + output_tokens: 1, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: currentHourStart, + requested_model: 'anthropic/claude-sonnet-4', + model: 'anthropic/claude-sonnet-4', + provider: 'anthropic', + inference_provider: 'anthropic', + }); await captureCostInsightSpend(db, { owner, actorUserId: userId, @@ -317,9 +335,183 @@ describe('Cost Insights evaluation integration', () => { expect(event?.snapshot.topDrivers).toEqual([ expect.objectContaining({ spendCategory: 'variable', - productKey: 'cli', + modelOrPlanKey: 'anthropic/claude-sonnet-4', totalMicrodollars: 30_000_000, }), ]); }); + + test('uses historical asOf for partial-hour anomaly amount and drivers', async () => { + const userId = await createUser(); + const owner = { type: 'user', id: userId } as const; + const hourStart = '2026-06-26T10:00:00.000Z'; + const asOf = '2026-06-26T10:30:00.000Z'; + + await db.insert(cost_insight_owner_configs).values({ + owned_by_user_id: userId, + spend_alerts_enabled: true, + cost_suggestions_enabled: false, + }); + for (const spend of [ + { + occurredAt: '2026-06-26T10:15:00.000Z', + amountMicrodollars: 30_000_000, + model: 'anthropic/claude-sonnet-4', + }, + { + occurredAt: '2026-06-26T10:45:00.000Z', + amountMicrodollars: 100_000_000, + model: 'openai/gpt-4.1', + }, + ]) { + await db.insert(microdollar_usage).values({ + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: spend.amountMicrodollars, + input_tokens: 1, + output_tokens: 1, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: spend.occurredAt, + requested_model: spend.model, + model: spend.model, + provider: spend.model.startsWith('openai') ? 'openai' : 'anthropic', + inference_provider: spend.model.startsWith('openai') ? 'openai' : 'anthropic', + }); + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt: spend.occurredAt, + amountMicrodollars: spend.amountMicrodollars, + category: 'variable', + source: 'ai_gateway', + productKey: 'cli', + featureKey: 'messages', + modelOrPlanKey: spend.model, + providerKey: spend.model.startsWith('openai') ? 'openai' : 'anthropic', + }); + } + + const result = await evaluateCostInsightsForOwner(db, owner, { asOf }); + const [event] = await db + .select({ snapshot: cost_insight_events.snapshot }) + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, userId)); + + expect(result.anomalyEventCreated).toBe(true); + expect(event?.snapshot).toMatchObject({ + currentHourVariableMicrodollars: 30_000_000, + topDriversWindow: { + startInclusive: hourStart, + endExclusive: asOf, + spendCategory: 'variable', + }, + topDrivers: [ + expect.objectContaining({ + modelOrPlanKey: 'anthropic/claude-sonnet-4', + totalMicrodollars: 30_000_000, + }), + ], + }); + }); + + test('hourly recovery evaluates the just-completed hour at rollover', async () => { + const userId = await createUser(); + const owner = { type: 'user', id: userId } as const; + const completedHourStart = '2026-06-26T10:00:00.000Z'; + const completedHourEnd = '2026-06-26T11:00:00.000Z'; + const asOf = '2026-06-26T11:05:00.000Z'; + const occurredAt = '2026-06-26T10:59:59.000Z'; + + await db.insert(cost_insight_owner_configs).values({ + owned_by_user_id: userId, + spend_alerts_enabled: true, + cost_suggestions_enabled: false, + }); + await db.insert(microdollar_usage).values({ + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 30_000_000, + input_tokens: 1, + output_tokens: 1, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: occurredAt, + requested_model: 'anthropic/claude-sonnet-4', + model: 'anthropic/claude-sonnet-4', + provider: 'anthropic', + inference_provider: 'anthropic', + }); + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt, + amountMicrodollars: 30_000_000, + category: 'variable', + source: 'ai_gateway', + productKey: 'cli', + featureKey: 'messages', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }); + + const result = await evaluateCostInsightsForOwner(db, owner, { + asOf, + recoverCompletedHour: true, + }); + const [event] = await db + .select({ snapshot: cost_insight_events.snapshot }) + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, userId)); + + expect(result.recoveredAnomalyEventCreated).toBe(true); + expect(result.anomalyEventCreated).toBe(false); + expect(event?.snapshot.topDriversWindow).toEqual({ + startInclusive: completedHourStart, + endExclusive: completedHourEnd, + spendCategory: 'variable', + }); + }); + + test('coalesces multiple spend captures into one durable owner evaluation', async () => { + const userId = await createUser(); + const owner = { type: 'user', id: userId } as const; + const occurredAt = '2026-06-26T10:15:00.000Z'; + + await db.insert(cost_insight_owner_configs).values({ + owned_by_user_id: userId, + spend_alerts_enabled: false, + cost_suggestions_enabled: false, + }); + for (let index = 0; index < 3; index += 1) { + await captureCostInsightSpend(db, { + owner, + actorUserId: userId, + occurredAt, + amountMicrodollars: 1_000_000, + category: 'variable', + source: 'ai_gateway', + productKey: 'cli', + featureKey: 'messages', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + }); + } + + const [dirtyOwner] = await db + .select() + .from(cost_insight_evaluation_dirty_owners) + .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)); + expect(dirtyOwner?.generation).toBe(3); + + await expect( + processPendingCostInsightEvaluations(db, { owner, asOf: '2026-06-26T10:30:00.000Z' }) + ).resolves.toMatchObject({ claimed: 1, evaluatedOwners: [owner], failedOwners: [] }); + await expect( + db + .select() + .from(cost_insight_evaluation_dirty_owners) + .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)) + ).resolves.toHaveLength(0); + }); }); diff --git a/apps/web/src/lib/cost-insights/evaluation.ts b/apps/web/src/lib/cost-insights/evaluation.ts index f48b4df584..ba394577f9 100644 --- a/apps/web/src/lib/cost-insights/evaluation.ts +++ b/apps/web/src/lib/cost-insights/evaluation.ts @@ -1,16 +1,16 @@ import { createHash } from 'node:crypto'; import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; +import { after } from 'next/server'; import type { CostInsightSpendCategory, CostInsightSpendSource } from '@kilocode/db/schema-types'; import { sql } from 'drizzle-orm'; import { db } from '@/lib/drizzle'; import { - getOwnerCurrentHourSpend, getOwnerHourlySpend, getOwnerRollingDriverEvidenceExact, getOwnerRollingSpendExact, - getOwnerTopSpendDrivers, + getOwnerSpendDriverEvidenceExact, type OwnerTopSpendDriver, } from './spend-repository'; import { @@ -60,6 +60,7 @@ export type CostInsightEvaluationSummary = { owner: CostInsightSpendOwner; evaluatedAt: string; anomalyEventCreated: boolean; + recoveredAnomalyEventCreated: boolean; thresholdEventCreated: boolean; threshold7DayEventCreated: boolean; threshold30DayEventCreated: boolean; @@ -166,48 +167,48 @@ export async function getCostInsightAnomalyPolicy( async function maybeCreateAnomalyAlert(params: { database: CostInsightDatabase; owner: CostInsightSpendOwner; - asOf: string; - currentHourStart: string; - currentHourVariableMicrodollars: number; + hourStart: string; + intervalEnd: string; + variableMicrodollars: number; anomalyPolicy: Awaited>; topDrivers: OwnerTopSpendDriver[]; }): Promise { - if (params.currentHourVariableMicrodollars < params.anomalyPolicy.thresholdMicrodollars) { + if (params.variableMicrodollars < params.anomalyPolicy.thresholdMicrodollars) { return false; } const dashboardState = await getCostInsightDashboardState(params.database, params.owner); - if (dashboardState.state?.activeAnomalyHourStart === params.currentHourStart) { + if (dashboardState.state?.activeAnomalyHourStart === params.hourStart) { return false; } + const snapshot = { + currentHourVariableMicrodollars: params.variableMicrodollars, + anomalyBaselineMicrodollars: params.anomalyPolicy.baselineMicrodollars, + anomalyThresholdMicrodollars: params.anomalyPolicy.thresholdMicrodollars, + topDrivers: topDriverSnapshot(params.topDrivers), + topDriversWindow: { + startInclusive: params.hourStart, + endExclusive: params.intervalEnd, + spendCategory: 'variable' as const, + }, + }; const event = await createCostInsightEvent(params.database, { owner: params.owner, eventType: 'anomaly_alert', alertKind: 'anomaly', title: 'Spend Anomaly Alert', - description: `Usage-based spend reached ${usdLabel( - params.currentHourVariableMicrodollars - )} in the current hour.`, - snapshot: { - currentHourVariableMicrodollars: params.currentHourVariableMicrodollars, - anomalyBaselineMicrodollars: params.anomalyPolicy.baselineMicrodollars, - anomalyThresholdMicrodollars: params.anomalyPolicy.thresholdMicrodollars, - topDrivers: topDriverSnapshot(params.topDrivers), - topDriversWindow: { - startInclusive: params.currentHourStart, - endExclusive: params.asOf, - spendCategory: 'variable', - }, - }, - dedupeKey: `anomaly:${params.currentHourStart}`, + description: `Usage-based spend reached ${usdLabel(params.variableMicrodollars)} during the evaluated UTC hour.`, + snapshot, + dedupeKey: `anomaly:${params.hourStart}`, }); if (!event.created) return false; await markCostInsightAnomalyEpisode(params.database, { owner: params.owner, eventId: event.id, - hourStart: params.currentHourStart, + hourStart: params.hourStart, + snapshot, }); await createCostInsightNotificationDeliveries( params.database, @@ -217,6 +218,36 @@ async function maybeCreateAnomalyAlert(params: { return true; } +async function evaluateAnomalyInterval(params: { + database: CostInsightDatabase; + owner: CostInsightSpendOwner; + hourStart: string; + intervalEnd: string; +}): Promise { + if (Date.parse(params.intervalEnd) <= Date.parse(params.hourStart)) return false; + + const anomalyPolicy = await getCostInsightAnomalyPolicy( + params.database, + params.owner, + params.hourStart + ); + const evidence = await getOwnerSpendDriverEvidenceExact(params.database, { + owner: params.owner, + startInclusive: params.hourStart, + endExclusive: params.intervalEnd, + category: 'variable', + }); + return await maybeCreateAnomalyAlert({ + database: params.database, + owner: params.owner, + hourStart: params.hourStart, + intervalEnd: params.intervalEnd, + variableMicrodollars: evidence.variableMicrodollars, + anomalyPolicy, + topDrivers: evidence.topDrivers, + }); +} + async function maybeCreateThresholdAlert(params: { database: CostInsightDatabase; owner: CostInsightSpendOwner; @@ -262,22 +293,23 @@ async function maybeCreateThresholdAlert(params: { if (evidence.totalMicrodollars < params.thresholdMicrodollars) return false; const descriptor = thresholdWindowDescriptors[params.alertKind]; + const snapshot = { + thresholdMicrodollars: params.thresholdMicrodollars, + thresholdWindow: descriptor.snapshotWindow, + ...descriptor.rollingSnapshot(evidence.totalMicrodollars), + topDrivers: topDriverSnapshot(evidence.topDrivers), + topDriversWindow: { + startInclusive: evidence.windowStart, + endExclusive: evidence.asOf, + }, + }; const event = await createCostInsightEvent(params.database, { owner: params.owner, eventType: 'threshold_crossed', alertKind: params.alertKind, title: `${descriptor.windowLabel} Spend Threshold Alert`, description: `Rolling ${descriptor.windowLabel} Credit spend crossed ${usdLabel(params.thresholdMicrodollars)}.`, - snapshot: { - thresholdMicrodollars: params.thresholdMicrodollars, - thresholdWindow: descriptor.snapshotWindow, - ...descriptor.rollingSnapshot(evidence.totalMicrodollars), - topDrivers: topDriverSnapshot(evidence.topDrivers), - topDriversWindow: { - startInclusive: evidence.windowStart, - endExclusive: evidence.asOf, - }, - }, + snapshot, dedupeKey: `${params.alertKind}:${params.thresholdMicrodollars}:${params.asOf}`, }); if (!event.created) return false; @@ -287,6 +319,7 @@ async function maybeCreateThresholdAlert(params: { eventId: event.id, crossedAt: params.asOf, alertKind: params.alertKind, + snapshot, }); await createCostInsightNotificationDeliveries( params.database, @@ -302,7 +335,6 @@ async function maybeCreateCostSuggestion(params: { topDrivers: OwnerTopSpendDriver[]; evidenceWindowStart: string; evidenceWindowEnd: string; - evidenceWindowDays: number; observedMicrodollars: number; }): Promise { const activeSuggestions = await listActiveCostInsightSuggestions(params.database, params.owner); @@ -411,70 +443,48 @@ async function maybeCreateCostSuggestion(params: { async function evaluateCostInsightsForOwnerLocked( database: CostInsightDatabase, owner: CostInsightSpendOwner, - options: { asOf?: string } = {} + options: { asOf?: string; recoverCompletedHour?: boolean } = {} ): Promise { - const asOf = options.asOf ?? new Date().toISOString(); + const requestedAsOf = options.asOf ?? new Date().toISOString(); + const asOfTimestamp = Date.parse(requestedAsOf); + if (!Number.isFinite(asOfTimestamp)) throw new Error('Cost Insights evaluation asOf is invalid.'); + const asOf = new Date(asOfTimestamp).toISOString(); const currentHourStart = floorUtcHour(new Date(asOf)); - const topDriverEnd = addHours(currentHourStart, 1); - const suggestionWindowEnd = topDriverEnd; + const suggestionWindowEnd = asOf; const suggestionWindowStart = addDays(suggestionWindowEnd, -7); const config = await getCostInsightOwnerConfig(database, owner); - const currentHourSpend = await getOwnerCurrentHourSpend(database, owner); - const rolling30DaySpendPromise = - config?.spend_alerts_enabled && config.spend_30_day_threshold_microdollars !== null - ? getOwnerRollingSpendExact(database, { + const suggestionEvidence = await getOwnerRollingDriverEvidenceExact(database, { + owner, + asOf, + windowHours: 7 * 24, + }); + const rolling24HourSpend = await getOwnerRollingSpendExact(database, { + owner, + asOf, + windowHours: 24, + }); + const rolling7DaySpend = + config?.spend_alerts_enabled && config.spend_7_day_threshold_microdollars !== null + ? await getOwnerRollingSpendExact(database, { owner, asOf, - windowHours: 30 * 24, + windowHours: 7 * 24, fallbackToCanonical: true, }) - : Promise.resolve({ totalMicrodollars: null }); - const rolling7DaySpendPromise = - config?.spend_alerts_enabled && config.spend_7_day_threshold_microdollars !== null - ? getOwnerRollingSpendExact(database, { + : { totalMicrodollars: null }; + const rolling30DaySpend = + config?.spend_alerts_enabled && config.spend_30_day_threshold_microdollars !== null + ? await getOwnerRollingSpendExact(database, { owner, asOf, - windowHours: 7 * 24, + windowHours: 30 * 24, fallbackToCanonical: true, }) - : Promise.resolve({ totalMicrodollars: null }); - const [ - anomalyTopDrivers, - suggestionTopDrivers, - suggestionHourlySpend, - rolling24HourSpend, - rolling7DaySpend, - rolling30DaySpend, - ] = await Promise.all([ - getOwnerTopSpendDrivers(database, { - owner, - startHour: currentHourStart, - endHourExclusive: topDriverEnd, - category: 'variable', - limit: 5, - }), - getOwnerTopSpendDrivers(database, { - owner, - startHour: suggestionWindowStart, - endHourExclusive: suggestionWindowEnd, - limit: 5, - }), - getOwnerHourlySpend(database, { - owner, - startHour: suggestionWindowStart, - endHourExclusive: suggestionWindowEnd, - }), - getOwnerRollingSpendExact(database, { owner, asOf, windowHours: 24 }), - rolling7DaySpendPromise, - rolling30DaySpendPromise, - ]); - const suggestionObservedMicrodollars = suggestionHourlySpend.reduce( - (sum, hour) => sum + (hour.variableMicrodollars ?? 0) + (hour.scheduledMicrodollars ?? 0), - 0 - ); + : { totalMicrodollars: null }; let anomalyEventCreated = false; + let recoveredAnomalyEventCreated = false; let thresholdEventCreated = false; let threshold7DayEventCreated = false; let threshold30DayEventCreated = false; @@ -482,15 +492,20 @@ async function evaluateCostInsightsForOwnerLocked( if (config?.spend_alerts_enabled) { if (config.anomaly_alerts_enabled) { - const anomalyPolicy = await getCostInsightAnomalyPolicy(database, owner, currentHourStart); - anomalyEventCreated = await maybeCreateAnomalyAlert({ + if (options.recoverCompletedHour) { + const completedHourStart = addHours(currentHourStart, -1); + recoveredAnomalyEventCreated = await evaluateAnomalyInterval({ + database, + owner, + hourStart: completedHourStart, + intervalEnd: currentHourStart, + }); + } + anomalyEventCreated = await evaluateAnomalyInterval({ database, owner, - asOf, - currentHourStart, - currentHourVariableMicrodollars: currentHourSpend.variableMicrodollars, - anomalyPolicy, - topDrivers: anomalyTopDrivers, + hourStart: currentHourStart, + intervalEnd: asOf, }); } thresholdEventCreated = await maybeCreateThresholdAlert({ @@ -523,11 +538,10 @@ async function evaluateCostInsightsForOwnerLocked( suggestionCreated = await maybeCreateCostSuggestion({ database, owner, - topDrivers: suggestionTopDrivers, + topDrivers: suggestionEvidence.topDrivers, evidenceWindowStart: suggestionWindowStart, evidenceWindowEnd: suggestionWindowEnd, - evidenceWindowDays: 7, - observedMicrodollars: suggestionObservedMicrodollars, + observedMicrodollars: suggestionEvidence.totalMicrodollars, }); } @@ -536,6 +550,7 @@ async function evaluateCostInsightsForOwnerLocked( owner, evaluatedAt: asOf, anomalyEventCreated, + recoveredAnomalyEventCreated, thresholdEventCreated, threshold7DayEventCreated, threshold30DayEventCreated, @@ -546,7 +561,7 @@ async function evaluateCostInsightsForOwnerLocked( export async function evaluateCostInsightsForOwner( database: CostInsightRootDatabase, owner: CostInsightSpendOwner, - options: { asOf?: string } = {} + options: { asOf?: string; recoverCompletedHour?: boolean } = {} ): Promise { return await database.transaction(async tx => { const lockKey = `cost-insights-evaluation:${owner.type}:${owner.id}`; @@ -557,17 +572,167 @@ export async function evaluateCostInsightsForOwner( }); } +const COST_INSIGHT_EVALUATION_LEASE_MINUTES = 5; + +type CostInsightClaimedDirtyOwner = { + id: string; + owned_by_user_id: string | null; + owned_by_organization_id: string | null; + generation: string | number | bigint; +}; + +export type CostInsightDirtyEvaluationSummary = { + claimed: number; + evaluatedOwners: CostInsightSpendOwner[]; + failedOwners: Array<{ owner: CostInsightSpendOwner; error: string }>; +}; + +function ownerFromDirtyRow(row: CostInsightClaimedDirtyOwner): CostInsightSpendOwner { + if (row.owned_by_user_id) return { type: 'user', id: row.owned_by_user_id }; + if (row.owned_by_organization_id) { + return { type: 'organization', id: row.owned_by_organization_id }; + } + throw new Error('Cost Insights dirty evaluation row has no owner.'); +} + +async function claimDirtyCostInsightOwners( + database: CostInsightRootDatabase, + options: { limit: number; owner?: CostInsightSpendOwner } +): Promise { + const ownerPredicate = options.owner + ? options.owner.type === 'user' + ? sql`dirty_owner.owned_by_user_id = ${options.owner.id} AND dirty_owner.owned_by_organization_id IS NULL` + : sql`dirty_owner.owned_by_organization_id = ${options.owner.id} AND dirty_owner.owned_by_user_id IS NULL` + : sql`TRUE`; + const result = await database.execute(sql` + WITH claimed AS ( + SELECT dirty_owner.id + FROM cost_insight_evaluation_dirty_owners dirty_owner + WHERE dirty_owner.next_attempt_at <= CURRENT_TIMESTAMP + AND ( + dirty_owner.claimed_at IS NULL + OR dirty_owner.claimed_at <= CURRENT_TIMESTAMP - make_interval( + mins => ${COST_INSIGHT_EVALUATION_LEASE_MINUTES} + ) + ) + AND ${ownerPredicate} + ORDER BY dirty_owner.dirty_at ASC, dirty_owner.id ASC + LIMIT ${options.limit} + FOR UPDATE SKIP LOCKED + ) + UPDATE cost_insight_evaluation_dirty_owners dirty_owner + SET + claimed_at = CURRENT_TIMESTAMP, + attempt_count = dirty_owner.attempt_count + 1, + last_error_redacted = NULL, + updated_at = CURRENT_TIMESTAMP + FROM claimed + WHERE dirty_owner.id = claimed.id + RETURNING + dirty_owner.id, + dirty_owner.owned_by_user_id, + dirty_owner.owned_by_organization_id, + dirty_owner.generation + `); + return result.rows; +} + +async function completeDirtyCostInsightOwner( + database: CostInsightRootDatabase, + row: CostInsightClaimedDirtyOwner +): Promise { + await database.execute(sql` + WITH removed AS ( + DELETE FROM cost_insight_evaluation_dirty_owners + WHERE id = ${row.id} + AND generation = ${row.generation} + RETURNING id + ) + UPDATE cost_insight_evaluation_dirty_owners dirty_owner + SET + claimed_at = NULL, + attempt_count = 0, + next_attempt_at = CURRENT_TIMESTAMP, + last_error_redacted = NULL, + updated_at = CURRENT_TIMESTAMP + WHERE dirty_owner.id = ${row.id} + AND NOT EXISTS (SELECT 1 FROM removed) + `); +} + +async function failDirtyCostInsightOwner( + database: CostInsightRootDatabase, + row: CostInsightClaimedDirtyOwner, + error: string +): Promise { + await database.execute(sql` + UPDATE cost_insight_evaluation_dirty_owners + SET + claimed_at = NULL, + next_attempt_at = CURRENT_TIMESTAMP + INTERVAL '5 minutes', + last_error_redacted = ${error.slice(0, 500)}, + updated_at = CURRENT_TIMESTAMP + WHERE id = ${row.id} + `); +} + +export async function processPendingCostInsightEvaluations( + database: CostInsightRootDatabase, + options: { + limit?: number; + owner?: CostInsightSpendOwner; + asOf?: string; + recoverCompletedHour?: boolean; + } = {} +): Promise { + const limit = options.limit ?? 25; + const summary: CostInsightDirtyEvaluationSummary = { + claimed: 0, + evaluatedOwners: [], + failedOwners: [], + }; + + while (summary.claimed < limit) { + const rows = await claimDirtyCostInsightOwners(database, { + limit: limit - summary.claimed, + owner: options.owner, + }); + if (rows.length === 0) break; + summary.claimed += rows.length; + + for (const row of rows) { + const owner = ownerFromDirtyRow(row); + try { + await evaluateCostInsightsForOwner(database, owner, { + asOf: options.asOf, + recoverCompletedHour: options.recoverCompletedHour, + }); + await completeDirtyCostInsightOwner(database, row); + if ( + !summary.evaluatedOwners.some( + evaluatedOwner => evaluatedOwner.type === owner.type && evaluatedOwner.id === owner.id + ) + ) { + summary.evaluatedOwners.push(owner); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await failDirtyCostInsightOwner(database, row, message); + summary.failedOwners.push({ owner, error: message }); + } + } + } + + return summary; +} + export function scheduleCostInsightEvaluationAfterSpend(owner: CostInsightSpendOwner): void { if (process.env.NODE_ENV === 'test') return; - setTimeout(() => { - void evaluateCostInsightsForOwner(db, owner) - .then(() => dispatchPendingCostInsightNotifications(db, 10)) - .catch(error => { - console.error('[cost-insights] post-spend evaluation failed', { - ownerType: owner.type, - ownerId: owner.id, - error: error instanceof Error ? error.message : String(error), - }); - }); - }, 0); + after(async () => { + const evaluations = await processPendingCostInsightEvaluations(db, { limit: 2, owner }); + await dispatchPendingCostInsightNotifications(db, 10); + if (evaluations.failedOwners.length > 0) { + console.error('[cost-insights] post-spend evaluation failed', evaluations.failedOwners[0]); + } + }); } diff --git a/apps/web/src/lib/cost-insights/jobs.ts b/apps/web/src/lib/cost-insights/jobs.ts index 8f96f53160..ecd7455e76 100644 --- a/apps/web/src/lib/cost-insights/jobs.ts +++ b/apps/web/src/lib/cost-insights/jobs.ts @@ -7,23 +7,50 @@ import { type CostInsightDatabase, type CostInsightRootDatabase, } from './repository'; -import { evaluateCostInsightsForOwner } from './evaluation'; +import { evaluateCostInsightsForOwner, processPendingCostInsightEvaluations } from './evaluation'; export type CostInsightHourlySweepSummary = { evaluatedOwners: number; failedOwners: Array<{ owner: CostInsightSpendOwner; error: string }>; + dirtyEvaluations: Awaited>; notifications: Awaited>; }; +function ownerKey(owner: CostInsightSpendOwner): string { + return `${owner.type}:${owner.id}`; +} + export async function runCostInsightHourlySweep( - database: CostInsightRootDatabase + database: CostInsightRootDatabase, + options: { asOf?: string; dirtyOwnerLimit?: number } = {} ): Promise { - const owners = await listEnabledCostInsightOwners(database); - const failedOwners: CostInsightHourlySweepSummary['failedOwners'] = []; + const asOf = options.asOf ?? new Date().toISOString(); + const dirtyEvaluations = await processPendingCostInsightEvaluations(database, { + limit: options.dirtyOwnerLimit ?? 25, + asOf, + recoverCompletedHour: true, + }); + const claimedOwnerKeys = new Set( + [ + ...dirtyEvaluations.evaluatedOwners, + ...dirtyEvaluations.failedOwners.map(row => row.owner), + ].map(ownerKey) + ); + const owners = (await listEnabledCostInsightOwners(database)).filter( + owner => !claimedOwnerKeys.has(ownerKey(owner)) + ); + const failedOwners: CostInsightHourlySweepSummary['failedOwners'] = [ + ...dirtyEvaluations.failedOwners, + ]; + let evaluatedOwners = dirtyEvaluations.evaluatedOwners.length; for (const owner of owners) { try { - await evaluateCostInsightsForOwner(database, owner); + await evaluateCostInsightsForOwner(database, owner, { + asOf, + recoverCompletedHour: true, + }); + evaluatedOwners += 1; } catch (error) { failedOwners.push({ owner, @@ -33,8 +60,9 @@ export async function runCostInsightHourlySweep( } return { - evaluatedOwners: owners.length - failedOwners.length, + evaluatedOwners, failedOwners, + dirtyEvaluations, notifications: await dispatchPendingCostInsightNotifications(database), }; } diff --git a/apps/web/src/lib/cost-insights/notifications.integration.test.ts b/apps/web/src/lib/cost-insights/notifications.integration.test.ts new file mode 100644 index 0000000000..9344a97642 --- /dev/null +++ b/apps/web/src/lib/cost-insights/notifications.integration.test.ts @@ -0,0 +1,96 @@ +import { afterEach, describe, expect, test } from '@jest/globals'; +import { + cost_insight_events, + cost_insight_notification_deliveries, + kilocode_users, +} from '@kilocode/db/schema'; +import { eq } from 'drizzle-orm'; + +import { db } from '@/lib/drizzle'; +import { claimPendingCostInsightNotificationDeliveries } from './notifications'; + +const testUserIds = new Set(); + +async function createDelivery(): Promise<{ deliveryId: string; userId: string }> { + const userId = `cost-insights-notification-${crypto.randomUUID()}`; + testUserIds.add(userId); + await db.insert(kilocode_users).values({ + id: userId, + google_user_email: `${userId}@example.com`, + google_user_name: 'Cost Insights Notification Test', + google_user_image_url: 'https://example.com/avatar.png', + stripe_customer_id: `cus_${crypto.randomUUID()}`, + }); + const [event] = await db + .insert(cost_insight_events) + .values({ + owned_by_user_id: userId, + event_type: 'anomaly_alert', + alert_kind: 'anomaly', + title: 'Spend Anomaly Alert', + description: 'Test alert', + snapshot: {}, + }) + .returning({ id: cost_insight_events.id }); + if (!event) throw new Error('Test event insert returned no row.'); + const [delivery] = await db + .insert(cost_insight_notification_deliveries) + .values({ event_id: event.id, recipient_user_id: userId }) + .returning({ id: cost_insight_notification_deliveries.id }); + if (!delivery) throw new Error('Test delivery insert returned no row.'); + return { deliveryId: delivery.id, userId }; +} + +afterEach(async () => { + for (const userId of testUserIds) { + await db.delete(cost_insight_events).where(eq(cost_insight_events.owned_by_user_id, userId)); + await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); + } + testUserIds.clear(); +}); + +describe('Cost Insights notification claims', () => { + test('reclaims a stale sending row after interruption', async () => { + const { deliveryId } = await createDelivery(); + + const firstClaim = await claimPendingCostInsightNotificationDeliveries(db, 1); + expect(firstClaim.rows).toHaveLength(1); + expect(firstClaim.rows[0]?.attempt_count).toBe(1); + + await db + .update(cost_insight_notification_deliveries) + .set({ claimed_at: '2026-01-01T00:00:00.000Z' }) + .where(eq(cost_insight_notification_deliveries.id, deliveryId)); + + const reclaimed = await claimPendingCostInsightNotificationDeliveries(db, 1); + expect(reclaimed.terminalized).toBe(0); + expect(reclaimed.rows).toHaveLength(1); + expect(reclaimed.rows[0]).toMatchObject({ delivery_id: deliveryId, attempt_count: 2 }); + }); + + test('terminalizes a stale sending row after attempt exhaustion', async () => { + const { deliveryId } = await createDelivery(); + await db + .update(cost_insight_notification_deliveries) + .set({ + status: 'sending', + attempt_count: 5, + claimed_at: '2026-01-01T00:00:00.000Z', + }) + .where(eq(cost_insight_notification_deliveries.id, deliveryId)); + + const claim = await claimPendingCostInsightNotificationDeliveries(db, 1); + const [delivery] = await db + .select() + .from(cost_insight_notification_deliveries) + .where(eq(cost_insight_notification_deliveries.id, deliveryId)); + + expect(claim).toMatchObject({ rows: [], terminalized: 1 }); + expect(delivery).toMatchObject({ + status: 'skipped', + attempt_count: 5, + claimed_at: null, + last_error_redacted: 'stale_claim_attempts_exhausted', + }); + }); +}); diff --git a/apps/web/src/lib/cost-insights/notifications.ts b/apps/web/src/lib/cost-insights/notifications.ts index 40cc9cb49e..fd7fc5449c 100644 --- a/apps/web/src/lib/cost-insights/notifications.ts +++ b/apps/web/src/lib/cost-insights/notifications.ts @@ -13,7 +13,10 @@ import { import { costInsightOwnerBasePath } from './owner'; import { MICRODOLLARS_PER_USD, microdollarsToUsd } from './policy'; -type CostInsightClaimedDeliveryRow = { +const COST_INSIGHT_NOTIFICATION_MAX_ATTEMPTS = 5; +const COST_INSIGHT_NOTIFICATION_LEASE_MINUTES = 15; + +export type CostInsightClaimedDeliveryRow = { delivery_id: string; recipient_user_id: string; recipient_email: string; @@ -22,6 +25,7 @@ type CostInsightClaimedDeliveryRow = { title: string; description: string; alert_kind: CostInsightAlertKind | null; + attempt_count: number; snapshot: { thresholdMicrodollars?: number | null; rolling24HourMicrodollars?: number | null; @@ -36,6 +40,7 @@ export type CostInsightNotificationDispatchSummary = { claimed: number; sent: number; skipped: number; + terminalized: number; failed: number; }; @@ -90,17 +95,48 @@ function amountLabels(row: CostInsightClaimedDeliveryRow): { }; } -async function claimPendingDeliveries( +async function terminalizeExhaustedDeliveryClaims(database: CostInsightDatabase): Promise { + const result = await database.execute<{ id: string }>(sql` + UPDATE cost_insight_notification_deliveries delivery + SET + status = 'skipped', + claimed_at = NULL, + failed_at = NULL, + sent_at = NULL, + last_error_redacted = 'stale_claim_attempts_exhausted', + updated_at = CURRENT_TIMESTAMP + WHERE delivery.status = 'sending' + AND delivery.attempt_count >= ${COST_INSIGHT_NOTIFICATION_MAX_ATTEMPTS} + AND delivery.claimed_at <= CURRENT_TIMESTAMP - make_interval( + mins => ${COST_INSIGHT_NOTIFICATION_LEASE_MINUTES} + ) + RETURNING delivery.id + `); + return result.rows.length; +} + +export async function claimPendingCostInsightNotificationDeliveries( database: CostInsightDatabase, limit: number -): Promise { +): Promise<{ rows: CostInsightClaimedDeliveryRow[]; terminalized: number }> { + const terminalized = await terminalizeExhaustedDeliveryClaims(database); const result = await database.execute(sql` WITH claimed AS ( SELECT delivery.id FROM cost_insight_notification_deliveries delivery - WHERE delivery.status IN ('pending', 'failed') - AND delivery.next_attempt_at <= now() - AND delivery.attempt_count < 5 + WHERE delivery.attempt_count < ${COST_INSIGHT_NOTIFICATION_MAX_ATTEMPTS} + AND ( + ( + delivery.status IN ('pending', 'failed') + AND delivery.next_attempt_at <= CURRENT_TIMESTAMP + ) + OR ( + delivery.status = 'sending' + AND delivery.claimed_at <= CURRENT_TIMESTAMP - make_interval( + mins => ${COST_INSIGHT_NOTIFICATION_LEASE_MINUTES} + ) + ) + ) ORDER BY delivery.next_attempt_at ASC, delivery.id ASC LIMIT ${limit} FOR UPDATE SKIP LOCKED @@ -108,19 +144,20 @@ async function claimPendingDeliveries( UPDATE cost_insight_notification_deliveries delivery SET status = 'sending', - claimed_at = now(), + claimed_at = CURRENT_TIMESTAMP, failed_at = NULL, sent_at = NULL, last_error_redacted = NULL, attempt_count = delivery.attempt_count + 1, - updated_at = now() + updated_at = CURRENT_TIMESTAMP FROM claimed WHERE delivery.id = claimed.id - RETURNING delivery.id, delivery.recipient_user_id + RETURNING delivery.id, delivery.recipient_user_id, delivery.attempt_count ) SELECT updated.id AS delivery_id, updated.recipient_user_id, + updated.attempt_count, recipient.google_user_email AS recipient_email, event.owned_by_user_id, event.owned_by_organization_id, @@ -137,7 +174,7 @@ async function claimPendingDeliveries( INNER JOIN kilocode_users recipient ON recipient.id = updated.recipient_user_id ORDER BY updated.id ASC `); - return result.rows; + return { rows: result.rows, terminalized }; } async function markDeliverySent(database: CostInsightDatabase, deliveryId: string): Promise { @@ -191,11 +228,13 @@ export async function dispatchPendingCostInsightNotifications( database: CostInsightDatabase, limit = 25 ): Promise { - const rows = await claimPendingDeliveries(database, limit); + const claim = await claimPendingCostInsightNotificationDeliveries(database, limit); + const rows = claim.rows; const summary: CostInsightNotificationDispatchSummary = { claimed: rows.length, sent: 0, skipped: 0, + terminalized: claim.terminalized, failed: 0, }; diff --git a/apps/web/src/lib/cost-insights/policy.test.ts b/apps/web/src/lib/cost-insights/policy.test.ts index 56663c2d87..8c95e58d4a 100644 --- a/apps/web/src/lib/cost-insights/policy.test.ts +++ b/apps/web/src/lib/cost-insights/policy.test.ts @@ -5,6 +5,7 @@ import { calculateAnomalyPolicy, formatSpendThresholdUsd, parseSpendThresholdUsd, + requireSafeMicrodollars, } from './policy'; describe('Cost Insights policy', () => { @@ -26,6 +27,29 @@ describe('Cost Insights policy', () => { expect(() => parseSpendThresholdUsd('10.001')).toThrow('positive USD amount'); expect(() => parseSpendThresholdUsd('1,000')).toThrow('positive USD amount'); }); + + it('accepts the largest cent-precision threshold that converts safely', () => { + expect(parseSpendThresholdUsd('9007199254.74')).toBe(9_007_199_254_740_000); + }); + + it('rejects thresholds whose final microdollar value is unsafe', () => { + expect(() => parseSpendThresholdUsd('9007199254.75')).toThrow( + 'Spend threshold is too large. Enter a smaller USD amount.' + ); + expect(() => parseSpendThresholdUsd('999999999999999999999999.99')).toThrow( + 'Spend threshold is too large. Enter a smaller USD amount.' + ); + }); + }); + + describe('requireSafeMicrodollars', () => { + it('requires a positive safe integer', () => { + expect(requireSafeMicrodollars(1, 'Amount')).toBe(1); + expect(() => requireSafeMicrodollars(0, 'Amount')).toThrow('positive safe integer'); + expect(() => requireSafeMicrodollars(Number.MAX_SAFE_INTEGER + 1, 'Amount')).toThrow( + 'positive safe integer' + ); + }); }); describe('formatSpendThresholdUsd', () => { diff --git a/apps/web/src/lib/cost-insights/policy.ts b/apps/web/src/lib/cost-insights/policy.ts index 98179d0797..2677e72427 100644 --- a/apps/web/src/lib/cost-insights/policy.ts +++ b/apps/web/src/lib/cost-insights/policy.ts @@ -37,10 +37,19 @@ export function parseSpendThresholdUsd(value: string | null): number | null { const dollars = Number.parseInt(wholePart, 10); const cents = Number.parseInt(centsPart.padEnd(2, '0') || '0', 10); const totalCents = dollars * 100 + cents; - if (!Number.isSafeInteger(totalCents) || totalCents <= 0) { + if (totalCents <= 0) { throw new Error('Spend threshold must be greater than $0.00.'); } - return usdToMicrodollarsFromCents(totalCents); + if (!Number.isSafeInteger(totalCents)) { + throw new Error('Spend threshold is too large. Enter a smaller USD amount.'); + } + + const totalMicrodollars = usdToMicrodollarsFromCents(totalCents); + try { + return requireSafeMicrodollars(totalMicrodollars, 'Spend threshold'); + } catch { + throw new Error('Spend threshold is too large. Enter a smaller USD amount.'); + } } export function formatSpendThresholdUsd(value: number | null): string { @@ -52,8 +61,8 @@ export function formatSpendThresholdUsd(value: number | null): string { } export function requireSafeMicrodollars(value: number, fieldName: string): number { - if (!Number.isSafeInteger(value) || value < 0) { - throw new Error(`${fieldName} must be a non-negative safe integer.`); + if (!Number.isSafeInteger(value) || value <= 0) { + throw new Error(`${fieldName} must be a positive safe integer.`); } return value; } diff --git a/apps/web/src/lib/cost-insights/presenter.test.ts b/apps/web/src/lib/cost-insights/presenter.test.ts index fb9bc69795..2793fb976c 100644 --- a/apps/web/src/lib/cost-insights/presenter.test.ts +++ b/apps/web/src/lib/cost-insights/presenter.test.ts @@ -4,6 +4,9 @@ import { formatActiveCostInsightAlerts, formatActiveCostInsightSuggestions, formatCostInsightEvents, + formatSpendEvidence, + normalizeCostInsightTimestamp, + organizationMemberLimitsHref, spendRangeStartHour, } from './presenter'; @@ -22,16 +25,32 @@ describe('Cost Insights presenter', () => { const state = { state: { activeAnomalyEventId: 'evt-anomaly', + activeAnomalyEpisodeId: 'evt-anomaly', activeAnomalyHourStart: '2026-06-25T19:00:00.000Z', + activeAnomalySnapshot: null, activeAnomalyReviewedAt: null, activeThresholdEventId: 'evt-threshold', + activeThresholdEpisodeId: 'evt-threshold', thresholdCrossingActive: true, + activeThresholdSnapshot: null, thresholdReviewedAt: null, + active7DayThresholdEventId: null, + active7DayThresholdEpisodeId: null, + threshold7DayCrossingActive: false, + active7DayThresholdSnapshot: null, + threshold7DayReviewedAt: null, + active30DayThresholdEventId: null, + active30DayThresholdEpisodeId: null, + threshold30DayCrossingActive: false, + active30DayThresholdSnapshot: null, + threshold30DayReviewedAt: null, lastEvaluatedAt: '2026-06-25T19:02:00.000Z', }, events: [ { + id: 'evt-anomaly', event_type: 'anomaly_alert', + alert_kind: 'anomaly', snapshot: { currentHourVariableMicrodollars: 112_700_000, anomalyBaselineMicrodollars: 6_000_000, @@ -50,14 +69,16 @@ describe('Cost Insights presenter', () => { }, ], topDriversWindow: { - startInclusive: '2026-06-25T19:00:00.000Z', - endExclusive: '2026-06-25T20:00:00.000Z', + startInclusive: '2026-06-25 21:00:00+02', + endExclusive: '2026-06-25 22:00:00+02', spendCategory: 'variable', }, }, }, { + id: 'evt-threshold', event_type: 'threshold_crossed', + alert_kind: 'threshold', snapshot: { rolling24HourMicrodollars: 184_900_000, thresholdMicrodollars: 150_000_000, @@ -86,6 +107,7 @@ describe('Cost Insights presenter', () => { expect(formatActiveCostInsightAlerts(state, { type: 'user', id: 'personal-owner' })).toEqual([ { type: 'anomaly', + eventId: 'evt-anomaly', title: 'Spend is unusually high this hour', description: "Usage-based spend is well above this account's recent hourly pattern.", facts: [ @@ -117,6 +139,7 @@ describe('Cost Insights presenter', () => { }, { type: 'threshold', + eventId: 'evt-threshold', title: '24-hour spend threshold crossed', description: 'Spend reached $184.90 against the $150.00 threshold.', facts: [ @@ -153,40 +176,38 @@ describe('Cost Insights presenter', () => { const state = { state: { activeAnomalyEventId: null, + activeAnomalyEpisodeId: null, activeAnomalyHourStart: null, + activeAnomalySnapshot: null, activeAnomalyReviewedAt: null, activeThresholdEventId: null, + activeThresholdEpisodeId: null, thresholdCrossingActive: false, + activeThresholdSnapshot: null, thresholdReviewedAt: null, active7DayThresholdEventId: null, + active7DayThresholdEpisodeId: null, threshold7DayCrossingActive: false, + active7DayThresholdSnapshot: null, threshold7DayReviewedAt: null, active30DayThresholdEventId: 'evt-threshold-30d', + active30DayThresholdEpisodeId: 'evt-threshold-30d', threshold30DayCrossingActive: true, + active30DayThresholdSnapshot: null, threshold30DayReviewedAt: null, lastEvaluatedAt: '2026-06-25T19:02:00.000Z', }, events: [ { id: 'evt-threshold-30d', - owned_by_user_id: 'personal-owner', - owned_by_organization_id: null, event_type: 'threshold_crossed', alert_kind: 'threshold_30d', - suggestion_kind: null, - active_suggestion_id: null, - actor_user_id: null, - title: '30-day Spend Threshold Alert', - description: 'Rolling 30-day Credit spend crossed $1,000.00.', snapshot: { thresholdWindow: 'rolling_30d', rolling30DayMicrodollars: 1_250_000_000, thresholdMicrodollars: 1_000_000_000, topDrivers: [], }, - dedupe_key: 'threshold_30d:1000000000:2026-06-25T19:02:00.000Z', - occurred_at: '2026-06-25T19:02:00.000Z', - created_at: '2026-06-25T19:02:00.000Z', }, ], } as Parameters[0]; @@ -194,6 +215,7 @@ describe('Cost Insights presenter', () => { expect(formatActiveCostInsightAlerts(state, { type: 'user', id: 'personal-owner' })).toEqual([ { type: 'threshold_30d', + eventId: 'evt-threshold-30d', title: '30-day spend threshold crossed', description: 'Spend reached $1,250.00 against the $1,000.00 threshold.', facts: [ @@ -211,40 +233,38 @@ describe('Cost Insights presenter', () => { const state = { state: { activeAnomalyEventId: null, + activeAnomalyEpisodeId: null, activeAnomalyHourStart: null, + activeAnomalySnapshot: null, activeAnomalyReviewedAt: null, activeThresholdEventId: null, + activeThresholdEpisodeId: null, thresholdCrossingActive: false, + activeThresholdSnapshot: null, thresholdReviewedAt: null, active7DayThresholdEventId: 'evt-threshold-7d', + active7DayThresholdEpisodeId: 'evt-threshold-7d', threshold7DayCrossingActive: true, + active7DayThresholdSnapshot: null, threshold7DayReviewedAt: null, active30DayThresholdEventId: null, + active30DayThresholdEpisodeId: null, threshold30DayCrossingActive: false, + active30DayThresholdSnapshot: null, threshold30DayReviewedAt: null, lastEvaluatedAt: '2026-06-25T19:02:00.000Z', }, events: [ { id: 'evt-threshold-7d', - owned_by_user_id: 'personal-owner', - owned_by_organization_id: null, event_type: 'threshold_crossed', alert_kind: 'threshold_7d', - suggestion_kind: null, - active_suggestion_id: null, - actor_user_id: null, - title: '7-day Spend Threshold Alert', - description: 'Rolling 7-day Credit spend crossed $500.00.', snapshot: { thresholdWindow: 'rolling_7d', rolling7DayMicrodollars: 620_000_000, thresholdMicrodollars: 500_000_000, topDrivers: [], }, - dedupe_key: 'threshold_7d:500000000:2026-06-25T19:02:00.000Z', - occurred_at: '2026-06-25T19:02:00.000Z', - created_at: '2026-06-25T19:02:00.000Z', }, ], } as Parameters[0]; @@ -252,6 +272,7 @@ describe('Cost Insights presenter', () => { expect(formatActiveCostInsightAlerts(state, { type: 'user', id: 'personal-owner' })).toEqual([ { type: 'threshold_7d', + eventId: 'evt-threshold-7d', title: '7-day spend threshold crossed', description: 'Spend reached $620.00 against the $500.00 threshold.', facts: [ @@ -353,4 +374,195 @@ describe('Cost Insights presenter', () => { }, ]); }); + + it('preserves uncovered hourly evidence as unavailable instead of zero spend', () => { + expect( + formatSpendEvidence( + [ + { + hourStart: '2026-06-25 23:00:00+00', + variableMicrodollars: null, + scheduledMicrodollars: null, + totalMicrodollars: null, + variableRecordCount: null, + scheduledRecordCount: null, + isCovered: false, + }, + ], + '24h' + ) + ).toEqual([ + { + label: '23', + periodStart: '2026-06-25T23:00:00.000Z', + periodEndExclusive: '2026-06-26T00:00:00.000Z', + coverage: 'unavailable', + coveredHours: 0, + totalHours: 1, + variableUsd: null, + scheduledUsd: null, + }, + ]); + }); + + it('rejects covered evidence with missing category totals instead of coercing it to zero', () => { + expect(() => + formatSpendEvidence( + [ + { + hourStart: '2026-06-25T23:00:00.000Z', + variableMicrodollars: null, + scheduledMicrodollars: 1_000_000, + totalMicrodollars: null, + variableRecordCount: 0, + scheduledRecordCount: 1, + isCovered: true, + }, + ], + '24h' + ) + ).toThrow('Covered Cost Insights evidence must include both spend categories.'); + }); + + it('marks aggregate evidence partial without exposing an understated covered subtotal', () => { + const points = [ + { + hourStart: '2026-06-25T22:00:00.000Z', + variableMicrodollars: 2_000_000, + scheduledMicrodollars: 1_000_000, + totalMicrodollars: 3_000_000, + variableRecordCount: 1, + scheduledRecordCount: 1, + isCovered: true, + }, + { + hourStart: '2026-06-25T23:00:00.000Z', + variableMicrodollars: null, + scheduledMicrodollars: null, + totalMicrodollars: null, + variableRecordCount: null, + scheduledRecordCount: null, + isCovered: false, + }, + { + hourStart: '2026-06-26T00:00:00.000Z', + variableMicrodollars: null, + scheduledMicrodollars: null, + totalMicrodollars: null, + variableRecordCount: null, + scheduledRecordCount: null, + isCovered: false, + }, + ]; + + expect(formatSpendEvidence(points, '30d')).toEqual([ + { + label: 'Jun 25', + periodStart: '2026-06-25T22:00:00.000Z', + periodEndExclusive: '2026-06-26T00:00:00.000Z', + coverage: 'partial', + coveredHours: 1, + totalHours: 2, + variableUsd: null, + scheduledUsd: null, + }, + { + label: 'Jun 26', + periodStart: '2026-06-26T00:00:00.000Z', + periodEndExclusive: '2026-06-26T01:00:00.000Z', + coverage: 'unavailable', + coveredHours: 0, + totalHours: 1, + variableUsd: null, + scheduledUsd: null, + }, + ]); + }); + + it('retains period boundaries and sums only fully covered 90-day buckets', () => { + const points = Array.from({ length: 2 }, (_, index) => ({ + hourStart: `2026-06-25T${String(22 + index).padStart(2, '0')}:00:00.000Z`, + variableMicrodollars: 2_000_000, + scheduledMicrodollars: 1_000_000, + totalMicrodollars: 3_000_000, + variableRecordCount: 1, + scheduledRecordCount: 1, + isCovered: true, + })); + + expect(formatSpendEvidence(points, '90d')).toEqual([ + { + label: 'Jun 25', + periodStart: '2026-06-25T22:00:00.000Z', + periodEndExclusive: '2026-06-26T00:00:00.000Z', + coverage: 'complete', + coveredHours: 2, + totalHours: 2, + variableUsd: 4, + scheduledUsd: 2, + }, + ]); + }); + + it('normalizes production-shaped Postgres timestamps at the presentation boundary', () => { + expect(normalizeCostInsightTimestamp('2026-06-25 21:02:00+02')).toBe( + '2026-06-25T19:02:00.000Z' + ); + + const [event] = formatCostInsightEvents({ type: 'user', id: 'personal-owner' }, [ + { + id: 'event-1', + eventType: 'config_changed', + alertKind: null, + suggestionKind: null, + actorUserId: null, + actorName: null, + title: 'Settings changed', + description: 'Settings changed.', + snapshot: {}, + occurredAt: '2026-06-25 21:02:00+02', + }, + ]); + expect(event?.occurredAt).toBe('2026-06-25T19:02:00.000Z'); + }); + + it('links member limits only when presenter inputs prove eligibility and availability', () => { + const base = { + owner: { type: 'organization', id: 'organization-1' } as const, + plan: 'enterprise' as const, + usageLimitsEnabled: true, + }; + expect( + organizationMemberLimitsHref({ + ...base, + uiOwner: { type: 'organization', name: 'Org', authorizedRole: 'owner' }, + }) + ).toBe('/organizations/organization-1'); + expect( + organizationMemberLimitsHref({ + ...base, + uiOwner: { type: 'organization', name: 'Org', authorizedRole: 'admin' }, + }) + ).toBe('/organizations/organization-1'); + expect( + organizationMemberLimitsHref({ + ...base, + uiOwner: { type: 'organization', name: 'Org', authorizedRole: 'billing_manager' }, + }) + ).toBeUndefined(); + expect( + organizationMemberLimitsHref({ + ...base, + plan: 'teams', + uiOwner: { type: 'organization', name: 'Org', authorizedRole: 'owner' }, + }) + ).toBeUndefined(); + expect( + organizationMemberLimitsHref({ + ...base, + usageLimitsEnabled: false, + uiOwner: { type: 'organization', name: 'Org', authorizedRole: 'admin' }, + }) + ).toBeUndefined(); + }); }); diff --git a/apps/web/src/lib/cost-insights/presenter.ts b/apps/web/src/lib/cost-insights/presenter.ts index d565d6358a..8904e4336f 100644 --- a/apps/web/src/lib/cost-insights/presenter.ts +++ b/apps/web/src/lib/cost-insights/presenter.ts @@ -1,6 +1,6 @@ import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; -import { kilocode_users } from '@kilocode/db/schema'; -import { inArray } from 'drizzle-orm'; +import { kilocode_users, organizations } from '@kilocode/db/schema'; +import { eq, inArray } from 'drizzle-orm'; import type { ActivityFilter, @@ -158,44 +158,110 @@ function suggestionWindowDays(start: string, end: string): number { return Math.max(1, Math.round(elapsedDays)); } -function hourlyEvidence(points: OwnerHourlySpend[], range: SpendRange): SpendEvidencePoint[] { - if (range === '1h' || range === '24h' || range === '7d') { - return points.map(point => ({ - label: formatHourLabel(point.hourStart, range), - periodStart: point.hourStart, - variableUsd: microdollarsToUsd(point.variableMicrodollars ?? 0), - scheduledUsd: microdollarsToUsd(point.scheduledMicrodollars ?? 0), - })); +export function normalizeCostInsightTimestamp(value: string | Date): string { + return new Date(value).toISOString(); +} + +function requireCoveredAmounts(point: OwnerHourlySpend): { + variableMicrodollars: number; + scheduledMicrodollars: number; +} { + if (point.variableMicrodollars === null || point.scheduledMicrodollars === null) { + throw new Error('Covered Cost Insights evidence must include both spend categories.'); + } + return { + variableMicrodollars: point.variableMicrodollars, + scheduledMicrodollars: point.scheduledMicrodollars, + }; +} + +function presentEvidenceBucket(points: OwnerHourlySpend[]): SpendEvidencePoint { + const first = points[0]; + const last = points.at(-1); + if (!first || !last) { + throw new Error('Cost Insights evidence buckets must contain at least one hour.'); + } + + const covered = points.filter(point => point.isCovered); + const common = { + label: formatDayLabel(first.hourStart), + periodStart: normalizeCostInsightTimestamp(first.hourStart), + periodEndExclusive: addHours(normalizeCostInsightTimestamp(last.hourStart), 1), + coveredHours: covered.length, + totalHours: points.length, + }; + if (covered.length === 0) { + return { ...common, coverage: 'unavailable', variableUsd: null, scheduledUsd: null }; + } + if (covered.length !== points.length) { + return { ...common, coverage: 'partial', variableUsd: null, scheduledUsd: null }; } - const days = new Map(); + let variableMicrodollars = 0; + let scheduledMicrodollars = 0; + for (const point of covered) { + const amounts = requireCoveredAmounts(point); + variableMicrodollars += amounts.variableMicrodollars; + scheduledMicrodollars += amounts.scheduledMicrodollars; + } + return { + ...common, + coverage: 'complete', + variableUsd: microdollarsToUsd(variableMicrodollars), + scheduledUsd: microdollarsToUsd(scheduledMicrodollars), + }; +} + +function groupByUtcDay(points: OwnerHourlySpend[]): OwnerHourlySpend[][] { + const days = new Map(); for (const point of points) { - const key = point.hourStart.slice(0, 10); - const existing = days.get(key) ?? { timestamp: point.hourStart, variable: 0, scheduled: 0 }; - existing.variable += point.variableMicrodollars ?? 0; - existing.scheduled += point.scheduledMicrodollars ?? 0; - days.set(key, existing); + const key = normalizeCostInsightTimestamp(point.hourStart).slice(0, 10); + const day = days.get(key) ?? []; + day.push(point); + days.set(key, day); } + return [...days.values()]; +} +export function formatSpendEvidence( + points: OwnerHourlySpend[], + range: SpendRange +): SpendEvidencePoint[] { + if (range === '1h' || range === '24h' || range === '7d') { + return points.map(point => { + const common = { + label: formatHourLabel(point.hourStart, range), + periodStart: normalizeCostInsightTimestamp(point.hourStart), + periodEndExclusive: addHours(normalizeCostInsightTimestamp(point.hourStart), 1), + coveredHours: point.isCovered ? 1 : 0, + totalHours: 1, + }; + if (!point.isCovered) { + return { + ...common, + coverage: 'unavailable' as const, + variableUsd: null, + scheduledUsd: null, + }; + } + const amounts = requireCoveredAmounts(point); + return { + ...common, + coverage: 'complete' as const, + variableUsd: microdollarsToUsd(amounts.variableMicrodollars), + scheduledUsd: microdollarsToUsd(amounts.scheduledMicrodollars), + }; + }); + } + + const days = groupByUtcDay(points); if (range === '30d') { - return [...days.values()].map(day => ({ - label: formatDayLabel(day.timestamp), - variableUsd: microdollarsToUsd(day.variable), - scheduledUsd: microdollarsToUsd(day.scheduled), - })); + return days.map(presentEvidenceBucket); } - const values = [...days.values()]; const weeks: SpendEvidencePoint[] = []; - for (let index = 0; index < values.length; index += 7) { - const chunk = values.slice(index, index + 7); - weeks.push({ - label: formatDayLabel( - chunk[0]?.timestamp ?? points[0]?.hourStart ?? new Date().toISOString() - ), - variableUsd: microdollarsToUsd(chunk.reduce((sum, day) => sum + day.variable, 0)), - scheduledUsd: microdollarsToUsd(chunk.reduce((sum, day) => sum + day.scheduled, 0)), - }); + for (let index = 0; index < days.length; index += 7) { + weeks.push(presentEvidenceBucket(days.slice(index, index + 7).flat())); } return weeks; } @@ -208,7 +274,7 @@ async function loadRangeEvidence( ): Promise { const startHour = spendRangeStartHour(range, endHourExclusive); const points = await getOwnerHourlySpend(database, { owner, startHour, endHourExclusive }); - return hourlyEvidence(points, range); + return formatSpendEvidence(points, range); } async function loadTopDriversByRange( @@ -249,6 +315,43 @@ async function loadActorLabels(database: CostInsightDatabase, actorUserIds: stri return new Map(rows.map(row => [row.id, row.name])); } +export function organizationMemberLimitsHref(params: { + owner: CostInsightSpendOwner; + uiOwner: CostInsightsOwner; + plan: 'teams' | 'enterprise' | null; + usageLimitsEnabled: boolean; +}): string | undefined { + if ( + params.owner.type !== 'organization' || + params.uiOwner.type !== 'organization' || + (params.uiOwner.authorizedRole !== 'owner' && params.uiOwner.authorizedRole !== 'admin') || + params.plan !== 'enterprise' || + !params.usageLimitsEnabled + ) { + return undefined; + } + return `/organizations/${encodeURIComponent(params.owner.id)}`; +} + +async function loadOrganizationMemberLimitsHref( + database: CostInsightDatabase, + owner: CostInsightSpendOwner, + uiOwner: CostInsightsOwner +): Promise { + if (owner.type !== 'organization' || uiOwner.type !== 'organization') return undefined; + const [organization] = await database + .select({ plan: organizations.plan, settings: organizations.settings }) + .from(organizations) + .where(eq(organizations.id, owner.id)) + .limit(1); + return organizationMemberLimitsHref({ + owner, + uiOwner, + plan: organization?.plan ?? null, + usageLimitsEnabled: organization?.settings.enable_usage_limits === true, + }); +} + function mapDrivers( owner: CostInsightSpendOwner, drivers: OwnerTopSpendDriver[], @@ -292,7 +395,7 @@ function mapDrivers( function buildMetrics(params: { rolling24HourMicrodollars: number | null; - currentHourVariableMicrodollars: number; + currentHourVariableMicrodollars: number | null; anomalyBaselineMicrodollars: number; anomalyThresholdMicrodollars: number; thresholdMicrodollars: number | null; @@ -320,10 +423,13 @@ function buildMetrics(params: { label: 'Usage-based spend this hour', value: money(params.currentHourVariableMicrodollars), detail: - params.currentHourVariableMicrodollars >= params.anomalyThresholdMicrodollars - ? 'Above current alert level' - : `Typical hour: ${money(params.anomalyBaselineMicrodollars)}`, + params.currentHourVariableMicrodollars === null + ? 'Current-hour spend evidence is unavailable' + : params.currentHourVariableMicrodollars >= params.anomalyThresholdMicrodollars + ? 'Above current alert level' + : `Typical hour: ${money(params.anomalyBaselineMicrodollars)}`, tone: + params.currentHourVariableMicrodollars !== null && params.currentHourVariableMicrodollars >= params.anomalyThresholdMicrodollars ? 'warning' : 'neutral', @@ -379,8 +485,8 @@ export function formatActiveCostInsightAlerts( : 'Exact alert-hour scope is unavailable for this older alert.', ...(driverWindow ? { - periodStart: driverWindow.startInclusive, - periodEndExclusive: driverWindow.endExclusive, + periodStart: normalizeCostInsightTimestamp(driverWindow.startInclusive), + periodEndExclusive: normalizeCostInsightTimestamp(driverWindow.endExclusive), } : {}), drivers, @@ -393,6 +499,7 @@ export function formatActiveCostInsightAlerts( : undefined; alerts.push({ type: 'anomaly', + eventId: event.id, title: 'Spend is unusually high this hour', description: "Usage-based spend is well above this account's recent hourly pattern.", facts: [ @@ -445,8 +552,8 @@ export function formatActiveCostInsightAlerts( ? { title: `Top rolling ${presentation.windowLabel} spend drivers`, description: 'Captured when the threshold was crossed.', - periodStart: driverWindow.startInclusive, - periodEndExclusive: driverWindow.endExclusive, + periodStart: normalizeCostInsightTimestamp(driverWindow.startInclusive), + periodEndExclusive: normalizeCostInsightTimestamp(driverWindow.endExclusive), drivers, totalSpendUsd: microdollarsToUsd( rollingMicrodollars ?? @@ -457,6 +564,7 @@ export function formatActiveCostInsightAlerts( : undefined; alerts.push({ type: presentation.alertType, + eventId: event.id, title: `${presentation.windowLabel} spend threshold crossed`, description: `Spend reached ${moneyWithCents( rollingMicrodollars @@ -579,7 +687,7 @@ export function formatCostInsightEvents( type: event.eventType === 'alert_reviewed' ? 'reviewed' : event.eventType, title: event.title, description: event.description, - occurredAt: event.occurredAt, + occurredAt: normalizeCostInsightTimestamp(event.occurredAt), actorLabel: event.actorName ?? undefined, amountLabel: event.snapshot.rolling30DayMicrodollars !== undefined @@ -645,6 +753,7 @@ export async function buildCostInsightsDashboardData(params: { listCostInsightEvents(params.database, params.owner, { limit: 5 }), ]); const [ + evidenceThisHour, evidence24h, evidence7d, evidence30d, @@ -652,7 +761,9 @@ export async function buildCostInsightsDashboardData(params: { actorLabels, activeSuggestions, eventPreview, + memberLimitsHref, ] = await Promise.all([ + loadRangeEvidence(params.database, params.owner, '1h', endHourExclusive), loadRangeEvidence(params.database, params.owner, '24h', endHourExclusive), loadRangeEvidence(params.database, params.owner, '7d', endHourExclusive), loadRangeEvidence(params.database, params.owner, '30d', endHourExclusive), @@ -671,16 +782,11 @@ export async function buildCostInsightsDashboardData(params: { ? listActiveCostInsightSuggestions(params.database, params.owner) : [], mapEvents(params.database, params.owner, events), + loadOrganizationMemberLimitsHref(params.database, params.owner, params.uiOwner), ]); - const evidenceThisHour: SpendEvidencePoint[] = [ - { - label: formatHourLabel(currentHourStart, '1h'), - periodStart: currentHourStart, - variableUsd: microdollarsToUsd(currentHourSpend.variableMicrodollars), - scheduledUsd: microdollarsToUsd(currentHourSpend.scheduledMicrodollars), - }, - ]; + const currentHourVariableMicrodollars = + evidenceThisHour[0]?.coverage === 'complete' ? currentHourSpend.variableMicrodollars : null; const alerts = formatActiveCostInsightAlerts(dashboardState, params.owner, actorLabels); return { enabled: config?.spend_alerts_enabled ?? false, @@ -688,7 +794,7 @@ export async function buildCostInsightsDashboardData(params: { range: '7d', metrics: buildMetrics({ rolling24HourMicrodollars: rolling24HourSpend.totalMicrodollars, - currentHourVariableMicrodollars: currentHourSpend.variableMicrodollars, + currentHourVariableMicrodollars, anomalyBaselineMicrodollars: anomalyPolicy.baselineMicrodollars, anomalyThresholdMicrodollars: anomalyPolicy.thresholdMicrodollars, thresholdMicrodollars: config?.spend_threshold_microdollars ?? null, @@ -712,9 +818,12 @@ export async function buildCostInsightsDashboardData(params: { }, alerts, suggestions: formatActiveCostInsightSuggestions(activeSuggestions), - lastEvaluatedAt: dashboardState.state?.lastEvaluatedAt ?? null, + lastEvaluatedAt: dashboardState.state?.lastEvaluatedAt + ? normalizeCostInsightTimestamp(dashboardState.state.lastEvaluatedAt) + : null, baselineMode: anomalyPolicy.mode, eventPreview, + memberLimitsHref, }; } diff --git a/apps/web/src/lib/cost-insights/repository.ts b/apps/web/src/lib/cost-insights/repository.ts index 2baf0dedf5..5805cd77ff 100644 --- a/apps/web/src/lib/cost-insights/repository.ts +++ b/apps/web/src/lib/cost-insights/repository.ts @@ -17,7 +17,7 @@ import type { CostInsightEventType, CostInsightSuggestionKind, } from '@kilocode/db/schema-types'; -import { and, count, desc, eq, inArray, isNotNull, isNull, lt, or, sql } from 'drizzle-orm'; +import { and, count, desc, eq, inArray, isNull, lt, or, sql } from 'drizzle-orm'; import type { db, DrizzleTransaction } from '@/lib/drizzle'; import { @@ -69,6 +69,11 @@ export type CostInsightEventInput = { dedupeKey?: string | null; }; +type CostInsightEventWriter = ( + database: CostInsightDatabase, + input: CostInsightEventInput +) => Promise<{ id: string; created: boolean }>; + export type CostInsightSuggestionInput = { owner: CostInsightSpendOwner; suggestionKind: CostInsightSuggestionKind; @@ -156,6 +161,133 @@ export async function updateCostInsightOwnerConfig( return { previous, current }; } +export function getCostInsightConfigChanges( + previous: CostInsightOwnerConfig, + current: CostInsightOwnerConfig +): Record { + const fields: Record = {}; + const addChange = (key: string, oldValue: unknown, newValue: unknown) => { + if (oldValue !== newValue) fields[key] = { old: oldValue, new: newValue }; + }; + + addChange('spendAlertsEnabled', previous.spend_alerts_enabled, current.spend_alerts_enabled); + addChange( + 'anomalyAlertsEnabled', + previous.anomaly_alerts_enabled, + current.anomaly_alerts_enabled + ); + addChange( + 'costSuggestionsEnabled', + previous.cost_suggestions_enabled, + current.cost_suggestions_enabled + ); + addChange( + 'spendThresholdMicrodollars', + previous.spend_threshold_microdollars, + current.spend_threshold_microdollars + ); + addChange( + 'spend7DayThresholdMicrodollars', + previous.spend_7_day_threshold_microdollars, + current.spend_7_day_threshold_microdollars + ); + addChange( + 'spend30DayThresholdMicrodollars', + previous.spend_30_day_threshold_microdollars, + current.spend_30_day_threshold_microdollars + ); + return fields; +} + +export function getCostInsightSettingsSnapshot(config: CostInsightOwnerConfig) { + return { + spendAlertsEnabled: config.spend_alerts_enabled, + anomalyAlertsEnabled: config.anomaly_alerts_enabled, + costSuggestionsEnabled: config.cost_suggestions_enabled, + spendThresholdMicrodollars: config.spend_threshold_microdollars, + spend7DayThresholdMicrodollars: config.spend_7_day_threshold_microdollars, + spend30DayThresholdMicrodollars: config.spend_30_day_threshold_microdollars, + }; +} + +export async function updateCostInsightSettings( + database: CostInsightRootDatabase, + params: { + owner: CostInsightSpendOwner; + actorUserId: string; + patch: CostInsightConfigPatch; + } +): Promise<{ + previous: CostInsightOwnerConfig; + current: CostInsightOwnerConfig; + hasChanges: boolean; +}> { + return await database.transaction(async transaction => { + const { previous, current } = await updateCostInsightOwnerConfig( + transaction, + params.owner, + params.patch + ); + const changes = getCostInsightConfigChanges(previous, current); + const hasChanges = Object.keys(changes).length > 0; + const disabled = hasChanges && previous.spend_alerts_enabled && !current.spend_alerts_enabled; + + if (disabled) { + await clearCostInsightAlertState(transaction, params.owner); + } else { + if (previous.anomaly_alerts_enabled && !current.anomaly_alerts_enabled) { + await clearCostInsightAnomalyEpisode(transaction, params.owner); + } + if ( + previous.spend_threshold_microdollars !== null && + current.spend_threshold_microdollars === null + ) { + await clearCostInsightThresholdEpisode(transaction, params.owner, null, 'threshold'); + } + if ( + previous.spend_7_day_threshold_microdollars !== null && + current.spend_7_day_threshold_microdollars === null + ) { + await clearCostInsightThresholdEpisode(transaction, params.owner, null, 'threshold_7d'); + } + if ( + previous.spend_30_day_threshold_microdollars !== null && + current.spend_30_day_threshold_microdollars === null + ) { + await clearCostInsightThresholdEpisode(transaction, params.owner, null, 'threshold_30d'); + } + } + + if (disabled) { + await createCostInsightEvent(transaction, { + owner: params.owner, + eventType: 'disabled', + actorUserId: params.actorUserId, + title: 'Spend Alerts turned off', + description: 'Spend Alerts were disabled. Cost evidence remains visible.', + snapshot: { + changedFields: changes, + settings: getCostInsightSettingsSnapshot(current), + }, + }); + } else if (hasChanges && (previous.spend_alerts_enabled || current.spend_alerts_enabled)) { + await createCostInsightEvent(transaction, { + owner: params.owner, + eventType: 'config_changed', + actorUserId: params.actorUserId, + title: 'Cost Insights settings changed', + description: 'Spend Alert settings were updated.', + snapshot: { + changedFields: changes, + settings: getCostInsightSettingsSnapshot(current), + }, + }); + } + + return { previous, current, hasChanges }; + }); +} + export async function getOrCreateCostInsightOwnerState( database: CostInsightDatabase, owner: CostInsightSpendOwner @@ -184,21 +316,29 @@ export async function clearCostInsightAlertState( .update(cost_insight_owner_states) .set({ active_anomaly_event_id: null, + active_anomaly_episode_id: null, active_anomaly_hour_start: null, + active_anomaly_snapshot: null, active_anomaly_reviewed_at: null, threshold_crossing_active: false, active_threshold_event_id: null, + active_threshold_episode_id: null, threshold_crossing_started_at: null, + active_threshold_snapshot: null, threshold_reviewed_at: null, threshold_recovered_at: null, rolling_7_day_threshold_crossing_active: false, active_rolling_7_day_threshold_event_id: null, + active_rolling_7_day_threshold_episode_id: null, rolling_7_day_threshold_crossing_started_at: null, + active_rolling_7_day_threshold_snapshot: null, rolling_7_day_threshold_reviewed_at: null, rolling_7_day_threshold_recovered_at: null, rolling_30_day_threshold_crossing_active: false, active_rolling_30_day_threshold_event_id: null, + active_rolling_30_day_threshold_episode_id: null, rolling_30_day_threshold_crossing_started_at: null, + active_rolling_30_day_threshold_snapshot: null, rolling_30_day_threshold_reviewed_at: null, rolling_30_day_threshold_recovered_at: null, updated_at: sql`now()`, @@ -215,7 +355,9 @@ export async function clearCostInsightAnomalyEpisode( .update(cost_insight_owner_states) .set({ active_anomaly_event_id: null, + active_anomaly_episode_id: null, active_anomaly_hour_start: null, + active_anomaly_snapshot: null, active_anomaly_reviewed_at: null, updated_at: sql`now()`, }) @@ -398,19 +540,31 @@ export async function getCostInsightDashboardState( const [state] = await database .select({ activeAnomalyEventId: cost_insight_owner_states.active_anomaly_event_id, + activeAnomalyEpisodeId: cost_insight_owner_states.active_anomaly_episode_id, activeAnomalyHourStart: cost_insight_owner_states.active_anomaly_hour_start, + activeAnomalySnapshot: cost_insight_owner_states.active_anomaly_snapshot, activeAnomalyReviewedAt: cost_insight_owner_states.active_anomaly_reviewed_at, activeThresholdEventId: cost_insight_owner_states.active_threshold_event_id, + activeThresholdEpisodeId: cost_insight_owner_states.active_threshold_episode_id, thresholdCrossingActive: cost_insight_owner_states.threshold_crossing_active, + activeThresholdSnapshot: cost_insight_owner_states.active_threshold_snapshot, thresholdReviewedAt: cost_insight_owner_states.threshold_reviewed_at, active7DayThresholdEventId: cost_insight_owner_states.active_rolling_7_day_threshold_event_id, + active7DayThresholdEpisodeId: + cost_insight_owner_states.active_rolling_7_day_threshold_episode_id, threshold7DayCrossingActive: cost_insight_owner_states.rolling_7_day_threshold_crossing_active, + active7DayThresholdSnapshot: + cost_insight_owner_states.active_rolling_7_day_threshold_snapshot, threshold7DayReviewedAt: cost_insight_owner_states.rolling_7_day_threshold_reviewed_at, active30DayThresholdEventId: cost_insight_owner_states.active_rolling_30_day_threshold_event_id, + active30DayThresholdEpisodeId: + cost_insight_owner_states.active_rolling_30_day_threshold_episode_id, threshold30DayCrossingActive: cost_insight_owner_states.rolling_30_day_threshold_crossing_active, + active30DayThresholdSnapshot: + cost_insight_owner_states.active_rolling_30_day_threshold_snapshot, threshold30DayReviewedAt: cost_insight_owner_states.rolling_30_day_threshold_reviewed_at, lastEvaluatedAt: cost_insight_owner_states.last_evaluated_at, }) @@ -429,23 +583,76 @@ export async function getCostInsightDashboardState( eventIds.length === 0 ? [] : await database - .select() + .select({ + id: cost_insight_events.id, + event_type: cost_insight_events.event_type, + alert_kind: cost_insight_events.alert_kind, + snapshot: cost_insight_events.snapshot, + }) .from(cost_insight_events) .where(inArray(cost_insight_events.id, eventIds)); - return { state: state ?? null, events }; + const eventsById = new Map(events.map(event => [event.id, event])); + const activeAnomalyEpisodeId = state?.activeAnomalyEpisodeId ?? state?.activeAnomalyEventId; + if (activeAnomalyEpisodeId && state?.activeAnomalySnapshot) { + eventsById.set(activeAnomalyEpisodeId, { + id: activeAnomalyEpisodeId, + event_type: 'anomaly_alert', + alert_kind: 'anomaly', + snapshot: state.activeAnomalySnapshot, + }); + } + const thresholdSnapshots = [ + { + id: state?.activeThresholdEpisodeId, + fallbackId: state?.activeThresholdEventId, + snapshot: state?.activeThresholdSnapshot, + alertKind: 'threshold' as const, + }, + { + id: state?.active7DayThresholdEpisodeId, + fallbackId: state?.active7DayThresholdEventId, + snapshot: state?.active7DayThresholdSnapshot, + alertKind: 'threshold_7d' as const, + }, + { + id: state?.active30DayThresholdEpisodeId, + fallbackId: state?.active30DayThresholdEventId, + snapshot: state?.active30DayThresholdSnapshot, + alertKind: 'threshold_30d' as const, + }, + ]; + for (const threshold of thresholdSnapshots) { + const episodeId = threshold.id ?? threshold.fallbackId; + if (!episodeId || !threshold.snapshot) continue; + eventsById.set(episodeId, { + id: episodeId, + event_type: 'threshold_crossed', + alert_kind: threshold.alertKind, + snapshot: threshold.snapshot, + }); + } + + return { state: state ?? null, events: [...eventsById.values()] }; } export async function markCostInsightAnomalyEpisode( database: CostInsightDatabase, - params: { owner: CostInsightSpendOwner; eventId: string; hourStart: string } + params: { + owner: CostInsightSpendOwner; + eventId: string; + hourStart: string; + snapshot: CostInsightEventSnapshot; + } ): Promise { const state = await getOrCreateCostInsightOwnerState(database, params.owner); await database .update(cost_insight_owner_states) .set({ active_anomaly_event_id: params.eventId, + active_anomaly_episode_id: params.eventId, active_anomaly_hour_start: params.hourStart, + active_anomaly_snapshot: params.snapshot, active_anomaly_reviewed_at: null, updated_at: sql`now()`, }) @@ -459,6 +666,7 @@ export async function markCostInsightThresholdEpisode( eventId: string; crossedAt: string; alertKind: CostInsightThresholdAlertKind; + snapshot: CostInsightEventSnapshot; } ): Promise { const state = await getOrCreateCostInsightOwnerState(database, params.owner); @@ -467,7 +675,9 @@ export async function markCostInsightThresholdEpisode( return { rolling_7_day_threshold_crossing_active: true, active_rolling_7_day_threshold_event_id: params.eventId, + active_rolling_7_day_threshold_episode_id: params.eventId, rolling_7_day_threshold_crossing_started_at: params.crossedAt, + active_rolling_7_day_threshold_snapshot: params.snapshot, rolling_7_day_threshold_reviewed_at: null, rolling_7_day_threshold_recovered_at: null, updated_at: sql`now()`, @@ -477,7 +687,9 @@ export async function markCostInsightThresholdEpisode( return { rolling_30_day_threshold_crossing_active: true, active_rolling_30_day_threshold_event_id: params.eventId, + active_rolling_30_day_threshold_episode_id: params.eventId, rolling_30_day_threshold_crossing_started_at: params.crossedAt, + active_rolling_30_day_threshold_snapshot: params.snapshot, rolling_30_day_threshold_reviewed_at: null, rolling_30_day_threshold_recovered_at: null, updated_at: sql`now()`, @@ -486,7 +698,9 @@ export async function markCostInsightThresholdEpisode( return { threshold_crossing_active: true, active_threshold_event_id: params.eventId, + active_threshold_episode_id: params.eventId, threshold_crossing_started_at: params.crossedAt, + active_threshold_snapshot: params.snapshot, threshold_reviewed_at: null, threshold_recovered_at: null, updated_at: sql`now()`, @@ -510,7 +724,9 @@ export async function clearCostInsightThresholdEpisode( return { rolling_7_day_threshold_crossing_active: false, active_rolling_7_day_threshold_event_id: null, + active_rolling_7_day_threshold_episode_id: null, rolling_7_day_threshold_crossing_started_at: null, + active_rolling_7_day_threshold_snapshot: null, rolling_7_day_threshold_reviewed_at: null, rolling_7_day_threshold_recovered_at: recoveredAt, updated_at: sql`now()`, @@ -520,7 +736,9 @@ export async function clearCostInsightThresholdEpisode( return { rolling_30_day_threshold_crossing_active: false, active_rolling_30_day_threshold_event_id: null, + active_rolling_30_day_threshold_episode_id: null, rolling_30_day_threshold_crossing_started_at: null, + active_rolling_30_day_threshold_snapshot: null, rolling_30_day_threshold_reviewed_at: null, rolling_30_day_threshold_recovered_at: recoveredAt, updated_at: sql`now()`, @@ -529,7 +747,9 @@ export async function clearCostInsightThresholdEpisode( return { threshold_crossing_active: false, active_threshold_event_id: null, + active_threshold_episode_id: null, threshold_crossing_started_at: null, + active_threshold_snapshot: null, threshold_reviewed_at: null, threshold_recovered_at: recoveredAt, updated_at: sql`now()`, @@ -541,9 +761,15 @@ export async function clearCostInsightThresholdEpisode( .where(eq(cost_insight_owner_states.id, state.id)); } -export async function acknowledgeCostInsightAlert( +async function acknowledgeCostInsightAlertInTransaction( database: CostInsightDatabase, - params: { owner: CostInsightSpendOwner; alertKind: CostInsightAlertKind; actorUserId: string } + params: { + owner: CostInsightSpendOwner; + alertKind: CostInsightAlertKind; + eventId: string; + actorUserId: string; + }, + writeEvent: CostInsightEventWriter ): Promise { const state = await getOrCreateCostInsightOwnerState(database, params.owner); const now = sql`now()`; @@ -558,21 +784,21 @@ export async function acknowledgeCostInsightAlert( const activeEpisode = params.alertKind === 'anomaly' ? and( - isNotNull(cost_insight_owner_states.active_anomaly_event_id), + sql`COALESCE(${cost_insight_owner_states.active_anomaly_episode_id}, ${cost_insight_owner_states.active_anomaly_event_id}) = ${params.eventId}`, isNull(cost_insight_owner_states.active_anomaly_reviewed_at) ) : params.alertKind === 'threshold_7d' ? and( - isNotNull(cost_insight_owner_states.active_rolling_7_day_threshold_event_id), + sql`COALESCE(${cost_insight_owner_states.active_rolling_7_day_threshold_episode_id}, ${cost_insight_owner_states.active_rolling_7_day_threshold_event_id}) = ${params.eventId}`, isNull(cost_insight_owner_states.rolling_7_day_threshold_reviewed_at) ) : params.alertKind === 'threshold_30d' ? and( - isNotNull(cost_insight_owner_states.active_rolling_30_day_threshold_event_id), + sql`COALESCE(${cost_insight_owner_states.active_rolling_30_day_threshold_episode_id}, ${cost_insight_owner_states.active_rolling_30_day_threshold_event_id}) = ${params.eventId}`, isNull(cost_insight_owner_states.rolling_30_day_threshold_reviewed_at) ) : and( - isNotNull(cost_insight_owner_states.active_threshold_event_id), + sql`COALESCE(${cost_insight_owner_states.active_threshold_episode_id}, ${cost_insight_owner_states.active_threshold_event_id}) = ${params.eventId}`, isNull(cost_insight_owner_states.threshold_reviewed_at) ); const [acknowledged] = await database @@ -583,7 +809,7 @@ export async function acknowledgeCostInsightAlert( if (!acknowledged) return false; - await createCostInsightEvent(database, { + await writeEvent(database, { owner: params.owner, eventType: 'alert_reviewed', alertKind: params.alertKind, @@ -597,10 +823,25 @@ export async function acknowledgeCostInsightAlert( ? '30-day Spend Threshold Alert reviewed' : '24-hour Spend Threshold Alert reviewed', description: 'Alert acknowledgment recorded for the current episode.', + dedupeKey: `alert-reviewed:${params.eventId}`, }); return true; } +export async function acknowledgeCostInsightAlert( + database: CostInsightRootDatabase, + params: { + owner: CostInsightSpendOwner; + alertKind: CostInsightAlertKind; + eventId: string; + actorUserId: string; + } +): Promise { + return await database.transaction(async transaction => + acknowledgeCostInsightAlertInTransaction(transaction, params, createCostInsightEvent) + ); +} + export async function upsertCostInsightActiveSuggestion( database: CostInsightDatabase, input: CostInsightSuggestionInput @@ -659,9 +900,10 @@ export async function listActiveCostInsightSuggestions( ); } -export async function dismissCostInsightSuggestion( +async function dismissCostInsightSuggestionInTransaction( database: CostInsightDatabase, - params: { owner: CostInsightSpendOwner; suggestionId: string; actorUserId: string } + params: { owner: CostInsightSpendOwner; suggestionId: string; actorUserId: string }, + writeEvent: CostInsightEventWriter ): Promise { const [suggestion] = await database .update(cost_insight_active_suggestions) @@ -680,7 +922,7 @@ export async function dismissCostInsightSuggestion( .returning(); if (!suggestion) return null; - await createCostInsightEvent(database, { + await writeEvent(database, { owner: params.owner, eventType: 'suggestion_dismissed', suggestionKind: suggestion.suggestion_kind, @@ -697,10 +939,20 @@ export async function dismissCostInsightSuggestion( ctaHref: suggestion.cta_href, }, }, + dedupeKey: `suggestion-dismissed:${suggestion.id}`, }); return suggestion.suggestion_kind; } +export async function dismissCostInsightSuggestion( + database: CostInsightRootDatabase, + params: { owner: CostInsightSpendOwner; suggestionId: string; actorUserId: string } +): Promise { + return await database.transaction(async transaction => + dismissCostInsightSuggestionInTransaction(transaction, params, createCostInsightEvent) + ); +} + export async function hasCurrentCostInsightAccess( database: CostInsightDatabase, owner: CostInsightSpendOwner, @@ -775,19 +1027,19 @@ export async function ownerHasUnreviewedCostInsightAlert( or( and( isNull(cost_insight_owner_states.active_anomaly_reviewed_at), - sql`${cost_insight_owner_states.active_anomaly_event_id} IS NOT NULL` + sql`COALESCE(${cost_insight_owner_states.active_anomaly_episode_id}, ${cost_insight_owner_states.active_anomaly_event_id}) IS NOT NULL` ), and( isNull(cost_insight_owner_states.threshold_reviewed_at), - sql`${cost_insight_owner_states.active_threshold_event_id} IS NOT NULL` + sql`COALESCE(${cost_insight_owner_states.active_threshold_episode_id}, ${cost_insight_owner_states.active_threshold_event_id}) IS NOT NULL` ), and( isNull(cost_insight_owner_states.rolling_7_day_threshold_reviewed_at), - sql`${cost_insight_owner_states.active_rolling_7_day_threshold_event_id} IS NOT NULL` + sql`COALESCE(${cost_insight_owner_states.active_rolling_7_day_threshold_episode_id}, ${cost_insight_owner_states.active_rolling_7_day_threshold_event_id}) IS NOT NULL` ), and( isNull(cost_insight_owner_states.rolling_30_day_threshold_reviewed_at), - sql`${cost_insight_owner_states.active_rolling_30_day_threshold_event_id} IS NOT NULL` + sql`COALESCE(${cost_insight_owner_states.active_rolling_30_day_threshold_episode_id}, ${cost_insight_owner_states.active_rolling_30_day_threshold_event_id}) IS NOT NULL` ) ) ) @@ -796,46 +1048,53 @@ export async function ownerHasUnreviewedCostInsightAlert( return Boolean(row); } -export async function countOpenCostInsightReviewItems( +export async function countUnreviewedCostInsightAlerts( database: CostInsightDatabase, owner: CostInsightSpendOwner ): Promise { - const [config, state, suggestions] = await Promise.all([ - getCostInsightOwnerConfig(database, owner), - database - .select({ - activeAnomalyEventId: cost_insight_owner_states.active_anomaly_event_id, - activeAnomalyReviewedAt: cost_insight_owner_states.active_anomaly_reviewed_at, - activeThresholdEventId: cost_insight_owner_states.active_threshold_event_id, - thresholdReviewedAt: cost_insight_owner_states.threshold_reviewed_at, - active7DayThresholdEventId: - cost_insight_owner_states.active_rolling_7_day_threshold_event_id, - threshold7DayReviewedAt: cost_insight_owner_states.rolling_7_day_threshold_reviewed_at, - active30DayThresholdEventId: - cost_insight_owner_states.active_rolling_30_day_threshold_event_id, - threshold30DayReviewedAt: cost_insight_owner_states.rolling_30_day_threshold_reviewed_at, - }) - .from(cost_insight_owner_states) - .where(costInsightOwnerWhere(owner, cost_insight_owner_states)) - .limit(1), - database - .select({ value: count() }) - .from(cost_insight_active_suggestions) - .where( - and( - costInsightOwnerWhere(owner, cost_insight_active_suggestions), - isNull(cost_insight_active_suggestions.dismissed_at) - ) - ), - ]); - - const activeState = state[0]; - const alertCount = - (activeState?.activeAnomalyEventId && !activeState.activeAnomalyReviewedAt ? 1 : 0) + - (activeState?.activeThresholdEventId && !activeState.thresholdReviewedAt ? 1 : 0) + - (activeState?.active7DayThresholdEventId && !activeState.threshold7DayReviewedAt ? 1 : 0) + - (activeState?.active30DayThresholdEventId && !activeState.threshold30DayReviewedAt ? 1 : 0); - const suggestionCount = - (config?.cost_suggestions_enabled ?? true) ? (suggestions[0]?.value ?? 0) : 0; - return alertCount + suggestionCount; + const [state] = await database + .select({ + activeAnomalyEpisodeId: cost_insight_owner_states.active_anomaly_episode_id, + activeAnomalyEventId: cost_insight_owner_states.active_anomaly_event_id, + activeAnomalyReviewedAt: cost_insight_owner_states.active_anomaly_reviewed_at, + activeThresholdEpisodeId: cost_insight_owner_states.active_threshold_episode_id, + activeThresholdEventId: cost_insight_owner_states.active_threshold_event_id, + thresholdReviewedAt: cost_insight_owner_states.threshold_reviewed_at, + active7DayThresholdEpisodeId: + cost_insight_owner_states.active_rolling_7_day_threshold_episode_id, + active7DayThresholdEventId: cost_insight_owner_states.active_rolling_7_day_threshold_event_id, + threshold7DayReviewedAt: cost_insight_owner_states.rolling_7_day_threshold_reviewed_at, + active30DayThresholdEpisodeId: + cost_insight_owner_states.active_rolling_30_day_threshold_episode_id, + active30DayThresholdEventId: + cost_insight_owner_states.active_rolling_30_day_threshold_event_id, + threshold30DayReviewedAt: cost_insight_owner_states.rolling_30_day_threshold_reviewed_at, + }) + .from(cost_insight_owner_states) + .where(costInsightOwnerWhere(owner, cost_insight_owner_states)) + .limit(1); + + return ( + ((state?.activeAnomalyEpisodeId ?? state?.activeAnomalyEventId) && + !state.activeAnomalyReviewedAt + ? 1 + : 0) + + ((state?.activeThresholdEpisodeId ?? state?.activeThresholdEventId) && + !state.thresholdReviewedAt + ? 1 + : 0) + + ((state?.active7DayThresholdEpisodeId ?? state?.active7DayThresholdEventId) && + !state.threshold7DayReviewedAt + ? 1 + : 0) + + ((state?.active30DayThresholdEpisodeId ?? state?.active30DayThresholdEventId) && + !state.threshold30DayReviewedAt + ? 1 + : 0) + ); } + +export const costInsightRepositoryInternals = { + acknowledgeCostInsightAlertInTransaction, + dismissCostInsightSuggestionInTransaction, +}; diff --git a/apps/web/src/lib/cost-insights/spend-evidence-seed.test.ts b/apps/web/src/lib/cost-insights/spend-evidence-seed.test.ts new file mode 100644 index 0000000000..ec305c189e --- /dev/null +++ b/apps/web/src/lib/cost-insights/spend-evidence-seed.test.ts @@ -0,0 +1,87 @@ +import { + assertDisposableFullCoverageSafe, + parseSpendEvidenceArgs, +} from '../../../../../dev/seed/cost-insights/spend-evidence'; +import type { SQL } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; + +describe('Cost Insights spend-evidence seed', () => { + test('preserves global coverage by default', () => { + expect(parseSpendEvidenceArgs([])).toEqual({ + rollupMode: 'bootstrap', + coverageMode: 'preserve', + }); + }); + + test('requires explicit disposable full-coverage mode', () => { + expect( + parseSpendEvidenceArgs(['--coverage-mode', 'disposable-full', '--rollup-mode', 'healthy']) + ).toEqual({ + rollupMode: 'healthy', + coverageMode: 'disposable-full', + }); + }); + + test('rejects ambiguous coverage arguments', () => { + expect(() => + parseSpendEvidenceArgs(['--coverage-mode', 'preserve', '--coverage-mode', 'disposable-full']) + ).toThrow('Duplicate flag: --coverage-mode'); + expect(() => parseSpendEvidenceArgs(['--coverage-mode', 'global'])).toThrow( + 'Unknown coverage mode: global' + ); + }); + + test('refuses disposable coverage when unrelated evidence exists', async () => { + const dialect = new PgDialect(); + const statements: string[] = []; + const fakeDb = { + execute: async (query: SQL) => { + statements.push(dialect.sqlToQuery(query).sql); + return { + rows: [ + { + unrelated_canonical_count: '1', + unrelated_rollup_count: '2', + unresolved_degraded_count: '3', + }, + ], + }; + }, + }; + + await expect( + assertDisposableFullCoverageSafe( + fakeDb as never, + '2026-03-28T20:00:00.000Z', + '2026-06-26T21:00:00.000Z' + ) + ).rejects.toThrow( + 'found 1 unrelated canonical rows, 2 unrelated rollup rows, and 3 unrelated unresolved degraded intervals' + ); + expect(statements[0]).toContain('unrelated_canonical'); + expect(statements[0]).toContain('unrelated_rollups'); + expect(statements[0]).toContain('cost_insight_rollup_degraded_intervals'); + }); + + test('allows disposable coverage only after unrelated evidence verification passes', async () => { + const fakeDb = { + execute: async () => ({ + rows: [ + { + unrelated_canonical_count: '0', + unrelated_rollup_count: '0', + unresolved_degraded_count: '0', + }, + ], + }), + }; + + await expect( + assertDisposableFullCoverageSafe( + fakeDb as never, + '2026-03-28T20:00:00.000Z', + '2026-06-26T21:00:00.000Z' + ) + ).resolves.toBeUndefined(); + }); +}); diff --git a/apps/web/src/lib/cost-insights/spend-repository.test.ts b/apps/web/src/lib/cost-insights/spend-repository.test.ts index 2bca1779c6..3bd86af566 100644 --- a/apps/web/src/lib/cost-insights/spend-repository.test.ts +++ b/apps/web/src/lib/cost-insights/spend-repository.test.ts @@ -62,7 +62,7 @@ describe('Cost Insights spend repository', () => { test('returns covered sparse hours as zero and uncovered hours as null', async () => { const executor = executorReturning([ { - hour_start: '2026-06-01 00:00:00+00', + hour_start: '2026-06-01 02:00:00+02', variable_microdollars: '0', scheduled_microdollars: '0', variable_record_count: '0', diff --git a/apps/web/src/lib/cost-insights/spend-repository.ts b/apps/web/src/lib/cost-insights/spend-repository.ts index 66da5accb6..528230e38f 100644 --- a/apps/web/src/lib/cost-insights/spend-repository.ts +++ b/apps/web/src/lib/cost-insights/spend-repository.ts @@ -74,6 +74,15 @@ export type OwnerRollingSpendExact = { isComplete: boolean; }; +export type OwnerSpendDriverEvidenceExact = { + startInclusive: string; + endExclusive: string; + variableMicrodollars: number; + scheduledMicrodollars: number; + totalMicrodollars: number; + topDrivers: OwnerTopSpendDriver[]; +}; + export type OwnerRollingDriverEvidenceExact = { asOf: string; windowStart: string; @@ -163,11 +172,11 @@ function ownerPredicate( } function normalizeDatabaseTimestamp(value: string | Date, fieldName: string): string { - const timestamp = value instanceof Date ? value.getTime() : Date.parse(value); - if (!Number.isFinite(timestamp)) { + const timestamp = new Date(value); + if (!Number.isFinite(timestamp.getTime())) { throw new Error(`${fieldName} is not a valid timestamp.`); } - return new Date(timestamp).toISOString(); + return timestamp.toISOString(); } function normalizeNullableDatabaseTimestamp( @@ -635,42 +644,53 @@ function compareTopSpendDrivers(left: OwnerTopSpendDriver, right: OwnerTopSpendD return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; } -export async function getOwnerRollingDriverEvidenceExact( +export async function getOwnerSpendDriverEvidenceExact( primaryDatabase: ExactRollingDatabase, - params: { owner: CostInsightSpendOwner; windowHours: number; asOf?: string } -): Promise { - const requestedAsOf = - params.asOf === undefined ? undefined : requireUtcTimestamp(params.asOf, 'asOf'); + params: { + owner: CostInsightSpendOwner; + startInclusive: string; + endExclusive: string; + category?: CostInsightSpendCategory; + } +): Promise { + const startInclusive = requireUtcTimestamp(params.startInclusive, 'startInclusive'); + const endExclusive = requireUtcTimestamp(params.endExclusive, 'endExclusive'); + const intervalMilliseconds = Date.parse(endExclusive) - Date.parse(startInclusive); + if ( + intervalMilliseconds <= 0 || + intervalMilliseconds > COST_INSIGHT_MAX_HOURLY_BUCKETS * HOUR_MS + ) { + throw new Error( + 'Cost Insights exact driver interval must be greater than zero and at most 90 days.' + ); + } return primaryDatabase.transaction( async transaction => { - const asOfResult = await transaction.execute(sql` - SELECT COALESCE(${requestedAsOf ?? null}::timestamptz, CURRENT_TIMESTAMP) AS value - `); - const asOfRow = asOfResult.rows[0]; - if (!asOfRow) { - throw new Error('Cost Insights exact driver query could not establish an as-of value.'); - } - const asOf = normalizeDatabaseTimestamp(asOfRow.value, 'as_of'); - const windowStart = getRollingWindowFragments(asOf, params.windowHours).windowStart; const aggregation = await loadCanonicalCostInsightAggregation(transaction, { owner: params.owner, - startInclusive: windowStart, - endExclusive: asOf, + startInclusive, + endExclusive, }); - const variableMicrodollars = aggregation.totals + const totals = params.category + ? aggregation.totals.filter(total => total.category === params.category) + : aggregation.totals; + const drivers = params.category + ? aggregation.drivers.filter(driver => driver.category === params.category) + : aggregation.drivers; + const variableMicrodollars = totals .filter(total => total.category === 'variable') .reduce( (sum, total) => sumSafe(sum, total.totalMicrodollars, 'exact driver variable total'), 0 ); - const scheduledMicrodollars = aggregation.totals + const scheduledMicrodollars = totals .filter(total => total.category === 'scheduled') .reduce( (sum, total) => sumSafe(sum, total.totalMicrodollars, 'exact driver scheduled total'), 0 ); - const topDrivers = aggregation.drivers + const topDrivers = drivers .map(driver => ({ category: driver.category, source: driver.source, @@ -686,8 +706,8 @@ export async function getOwnerRollingDriverEvidenceExact( .slice(0, COST_INSIGHT_MAX_TOP_DRIVERS); return { - asOf, - windowStart, + startInclusive, + endExclusive, variableMicrodollars, scheduledMicrodollars, totalMicrodollars: sumSafe( @@ -702,6 +722,29 @@ export async function getOwnerRollingDriverEvidenceExact( ); } +export async function getOwnerRollingDriverEvidenceExact( + primaryDatabase: ExactRollingDatabase, + params: { owner: CostInsightSpendOwner; windowHours: number; asOf?: string } +): Promise { + const requestedAsOf = + params.asOf === undefined ? undefined : requireUtcTimestamp(params.asOf, 'asOf'); + const asOf = requestedAsOf ?? new Date().toISOString(); + const windowStart = getRollingWindowFragments(asOf, params.windowHours).windowStart; + const evidence = await getOwnerSpendDriverEvidenceExact(primaryDatabase, { + owner: params.owner, + startInclusive: windowStart, + endExclusive: asOf, + }); + return { + asOf, + windowStart, + variableMicrodollars: evidence.variableMicrodollars, + scheduledMicrodollars: evidence.scheduledMicrodollars, + totalMicrodollars: evidence.totalMicrodollars, + topDrivers: evidence.topDrivers, + }; +} + export async function getOwnerRolling24HourDriverEvidenceExact( primaryDatabase: ExactRollingDatabase, params: { owner: CostInsightSpendOwner; asOf?: string } diff --git a/apps/web/src/lib/exa-usage-log-indexes-script.test.ts b/apps/web/src/lib/exa-usage-log-indexes-script.test.ts index 00f57f40fa..990c22dda7 100644 --- a/apps/web/src/lib/exa-usage-log-indexes-script.test.ts +++ b/apps/web/src/lib/exa-usage-log-indexes-script.test.ts @@ -64,15 +64,17 @@ describe('Exa usage-log index operator', () => { expect(statements[0]).toContain('partition_class.relispartition'); }); - test('executes both concurrent indexes sequentially for selected catalog partitions', async () => { + test('inspects, creates, and verifies both concurrent indexes sequentially', async () => { const statements: string[] = []; + const catalogParams: unknown[][] = []; const dialect = new PgDialect(); + const validIndexes = new Set(); let activeExecutions = 0; let maximumActiveExecutions = 0; const fakeDb = { execute: async (query: SQL) => { - const statement = dialect.sqlToQuery(query).sql; - statements.push(statement); + const compiled = dialect.sqlToQuery(query); + statements.push(compiled.sql); if (statements.length === 1) { return { rows: [ @@ -81,8 +83,31 @@ describe('Exa usage-log index operator', () => { ], }; } + if (compiled.sql.includes('FROM pg_catalog.pg_index')) { + catalogParams.push(compiled.params); + const indexName = String(compiled.params[1]); + return { + rows: validIndexes.has(indexName) + ? [ + { + schema_name: 'public', + index_name: indexName, + partition_schema_name: 'public', + partition_name: 'exa_usage_log_2026_06', + is_valid: true, + is_ready: true, + }, + ] + : [], + }; + } + activeExecutions++; maximumActiveExecutions = Math.max(maximumActiveExecutions, activeExecutions); + const createdIndexName = compiled.sql.match( + /CREATE INDEX CONCURRENTLY IF NOT EXISTS "([^"]+)"/ + )?.[1]; + if (createdIndexName) validIndexes.add(createdIndexName); await Promise.resolve(); activeExecutions--; return { rows: [] }; @@ -101,9 +126,147 @@ describe('Exa usage-log index operator', () => { } expect(maximumActiveExecutions).toBe(1); - expect(statements.slice(1)).toEqual([ + expect(catalogParams).toEqual([ + ['public', 'exa_usage_log_2026_06_charged_created_at_idx'], + ['public', 'exa_usage_log_2026_06_charged_created_at_idx'], + ['public', 'exa_usage_log_2026_06_charged_org_created_at_idx'], + ['public', 'exa_usage_log_2026_06_charged_org_created_at_idx'], + ]); + expect(statements.filter(statement => statement.startsWith('CREATE INDEX'))).toEqual([ 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "exa_usage_log_2026_06_charged_created_at_idx" ON "public"."exa_usage_log_2026_06" ("created_at") WHERE "charged_to_balance" = true AND "cost_microdollars" > 0', 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "exa_usage_log_2026_06_charged_org_created_at_idx" ON "public"."exa_usage_log_2026_06" ("organization_id", "created_at") WHERE "organization_id" IS NOT NULL AND "charged_to_balance" = true AND "cost_microdollars" > 0', ]); + expect( + statements.filter(statement => statement.includes('FROM pg_catalog.pg_index')) + ).toHaveLength(4); + expect(statements[1]).toContain('index_namespace.nspname = $1'); + expect(statements[1]).toContain('index_catalog.indisvalid AS is_valid'); + expect(statements[1]).toContain('index_catalog.indisready AS is_ready'); + }); + + test('drops and rebuilds an interrupted invalid concurrent index', async () => { + const statements: string[] = []; + const dialect = new PgDialect(); + let firstIndexState: 'invalid' | 'missing' | 'valid' = 'invalid'; + const firstIndexName = 'exa_usage_log_2026_06_charged_created_at_idx'; + const secondIndexName = 'exa_usage_log_2026_06_charged_org_created_at_idx'; + const fakeDb = { + execute: async (query: SQL) => { + const compiled = dialect.sqlToQuery(query); + statements.push(compiled.sql); + if (statements.length === 1) { + return { rows: [{ schema_name: 'public', partition_name: 'exa_usage_log_2026_06' }] }; + } + if (compiled.sql.includes('FROM pg_catalog.pg_index')) { + const indexName = String(compiled.params[1]); + const state = indexName === firstIndexName ? firstIndexState : 'valid'; + return { + rows: + state === 'missing' + ? [] + : [ + { + schema_name: 'public', + index_name: indexName, + partition_schema_name: 'public', + partition_name: 'exa_usage_log_2026_06', + is_valid: state === 'valid', + is_ready: state === 'valid', + }, + ], + }; + } + if (compiled.sql.startsWith('DROP INDEX')) firstIndexState = 'missing'; + if (compiled.sql.includes(`"${firstIndexName}"`)) firstIndexState = 'valid'; + return { rows: [] }; + }, + }; + const log = jest.spyOn(console, 'log').mockImplementation(() => undefined); + + try { + await provisionHistoricalExaUsageLogIndexes(fakeDb as never, { + execute: true, + sleepMs: 0, + }); + } finally { + log.mockRestore(); + } + + expect(statements.filter(statement => statement.startsWith('DROP INDEX'))).toEqual([ + `DROP INDEX CONCURRENTLY IF EXISTS "public"."${firstIndexName}"`, + ]); + expect(statements.filter(statement => statement.startsWith('CREATE INDEX'))).toEqual([ + 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "exa_usage_log_2026_06_charged_created_at_idx" ON "public"."exa_usage_log_2026_06" ("created_at") WHERE "charged_to_balance" = true AND "cost_microdollars" > 0', + ]); + expect(statements.some(statement => statement.includes(`"${secondIndexName}"`))).toBe(false); + }); + + test('fails when final catalog state is not valid and ready', async () => { + const dialect = new PgDialect(); + let statementCount = 0; + const fakeDb = { + execute: async (query: SQL) => { + const compiled = dialect.sqlToQuery(query); + statementCount++; + if (statementCount === 1) { + return { rows: [{ schema_name: 'public', partition_name: 'exa_usage_log_2026_06' }] }; + } + if (compiled.sql.includes('FROM pg_catalog.pg_index')) { + return statementCount === 2 + ? { rows: [] } + : { + rows: [ + { + schema_name: 'public', + index_name: 'exa_usage_log_2026_06_charged_created_at_idx', + partition_schema_name: 'public', + partition_name: 'exa_usage_log_2026_06', + is_valid: false, + is_ready: true, + }, + ], + }; + } + return { rows: [] }; + }, + }; + const log = jest.spyOn(console, 'log').mockImplementation(() => undefined); + + try { + await expect( + provisionHistoricalExaUsageLogIndexes(fakeDb as never, { + execute: true, + sleepMs: 0, + }) + ).rejects.toThrow('is not valid and ready after provisioning'); + } finally { + log.mockRestore(); + } + }); + + test('rejects unsafe partition identifiers returned by the catalog', async () => { + const dialect = new PgDialect(); + const statements: string[] = []; + const fakeDb = { + execute: async (query: SQL) => { + statements.push(dialect.sqlToQuery(query).sql); + return { + rows: [ + { + schema_name: 'public', + partition_name: 'exa_usage_log_2026_06"; DROP TABLE exa_usage_log; --', + }, + ], + }; + }, + }; + + await expect( + provisionHistoricalExaUsageLogIndexes(fakeDb as never, { + execute: true, + sleepMs: 0, + }) + ).rejects.toThrow('Invalid Exa usage-log partition name'); + expect(statements).toHaveLength(1); }); }); diff --git a/apps/web/src/lib/exa-usage-partitions.test.ts b/apps/web/src/lib/exa-usage-partitions.test.ts index 17bdc25f56..f1c101e621 100644 --- a/apps/web/src/lib/exa-usage-partitions.test.ts +++ b/apps/web/src/lib/exa-usage-partitions.test.ts @@ -1,5 +1,6 @@ import { buildExaUsageLogPartitionIndexDefinitions, + buildExaUsageLogPartitionIndexDropStatement, provisionExaUsageLogPartitions, } from '@/lib/exa-usage-partitions'; import type { SQL } from 'drizzle-orm'; @@ -38,6 +39,17 @@ describe('Exa usage-log partition indexes', () => { ).toBe(true); }); + test('builds a schema-qualified concurrent drop for a controlled index name', () => { + expect( + buildExaUsageLogPartitionIndexDropStatement( + 'public', + 'exa_usage_log_2026_07_charged_created_at_idx' + ) + ).toBe( + 'DROP INDEX CONCURRENTLY IF EXISTS "public"."exa_usage_log_2026_07_charged_created_at_idx"' + ); + }); + test('rejects identifiers outside the expected catalog naming contract', () => { expect(() => buildExaUsageLogPartitionIndexDefinitions( @@ -53,6 +65,12 @@ describe('Exa usage-log partition indexes', () => { true ) ).toThrow('Unsafe PostgreSQL identifier'); + expect(() => + buildExaUsageLogPartitionIndexDropStatement( + 'public', + 'exa_usage_log_2026_07_charged_created_at_idx"; DROP TABLE exa_usage_log; --' + ) + ).toThrow('Invalid Exa usage-log partition index name'); }); test('provisions indexes only on write-free future partitions', async () => { diff --git a/apps/web/src/lib/exa-usage-partitions.ts b/apps/web/src/lib/exa-usage-partitions.ts index 97d895c9f4..dd23530c4b 100644 --- a/apps/web/src/lib/exa-usage-partitions.ts +++ b/apps/web/src/lib/exa-usage-partitions.ts @@ -16,6 +16,8 @@ export type ExaUsageLogPartitionIndexDefinition = { const POSTGRES_IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_]*$/; const EXA_USAGE_LOG_PARTITION_PATTERN = /^exa_usage_log_\d{4}_(?:0[1-9]|1[0-2])$/; +const EXA_USAGE_LOG_PARTITION_INDEX_PATTERN = + /^exa_usage_log_\d{4}_(?:0[1-9]|1[0-2])_charged_(?:org_)?created_at_idx$/; const POSTGRES_IDENTIFIER_MAX_LENGTH = 63; function quoteIdentifier(identifier: string): string { @@ -28,6 +30,17 @@ function quoteIdentifier(identifier: string): string { return `"${identifier}"`; } +export function buildExaUsageLogPartitionIndexDropStatement( + schemaName: string, + indexName: string +): string { + if (!EXA_USAGE_LOG_PARTITION_INDEX_PATTERN.test(indexName)) { + throw new Error(`Invalid Exa usage-log partition index name: ${indexName}`); + } + + return `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdentifier(schemaName)}.${quoteIdentifier(indexName)}`; +} + export function buildExaUsageLogPartitionIndexDefinitions( schemaName: string, partitionName: string, diff --git a/apps/web/src/routers/cost-insights-router.test.ts b/apps/web/src/routers/cost-insights-router.test.ts index 646541eae4..250cf6ad4c 100644 --- a/apps/web/src/routers/cost-insights-router.test.ts +++ b/apps/web/src/routers/cost-insights-router.test.ts @@ -8,6 +8,11 @@ import { import { eq } from 'drizzle-orm'; import { db } from '@/lib/drizzle'; +import { + acknowledgeCostInsightAlert, + costInsightRepositoryInternals, + updateCostInsightSettings, +} from '@/lib/cost-insights/repository'; import type { createCallerForUser as CreateCallerForUser } from '@/routers/test-utils'; import { insertTestUser } from '@/tests/helpers/user.helper'; @@ -65,7 +70,11 @@ describe('Cost Insights router', () => { spend7DayThresholdUsd: null, spend30DayThresholdUsd: null, }), - () => caller.costInsights.acknowledgeAlert({ alertKind: 'anomaly' }), + () => + caller.costInsights.acknowledgeAlert({ + alertKind: 'anomaly', + eventId: crypto.randomUUID(), + }), () => caller.costInsights.disableThreshold(), () => caller.costInsights.dismissSuggestion({ suggestionId: crypto.randomUUID() }), ]; @@ -109,7 +118,7 @@ describe('Cost Insights router', () => { expect(trackingMock.trackCostInsightsUiInteraction).toHaveBeenCalledTimes(1); }); - it('counts open alerts and suggestions for sidebar review badge', async () => { + it('counts only unreviewed Spend Alerts for sidebar attention', async () => { const user = await insertTestUser({ is_admin: true }); await db.insert(cost_insight_owner_configs).values({ owned_by_user_id: user.id, @@ -183,7 +192,7 @@ describe('Cost Insights router', () => { const caller = await createCallerForUser(user.id); await expect(caller.costInsights.getAttentionState()).resolves.toEqual({ attention: 'alert', - reviewItemCount: 5, + reviewItemCount: 4, }); await db @@ -285,6 +294,94 @@ describe('Cost Insights router', () => { active_anomaly_hour_start: null, active_anomaly_reviewed_at: null, }); + const events = await db + .select({ eventType: cost_insight_events.event_type }) + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, user.id)); + expect(events).toEqual([{ eventType: 'anomaly_alert' }]); + }); + + it('evaluates immediately after enabling Spend Alerts', async () => { + const user = await insertTestUser({ is_admin: true }); + const caller = await createCallerForUser(user.id); + + await expect( + caller.costInsights.updateSettings({ + spendAlertsEnabled: true, + anomalyAlertsEnabled: false, + costSuggestionsEnabled: false, + spendThresholdUsd: null, + spend7DayThresholdUsd: null, + spend30DayThresholdUsd: null, + }) + ).resolves.toEqual({ success: true }); + + const [config] = await db + .select({ enabled: cost_insight_owner_configs.spend_alerts_enabled }) + .from(cost_insight_owner_configs) + .where(eq(cost_insight_owner_configs.owned_by_user_id, user.id)); + const [state] = await db + .select({ lastEvaluatedAt: cost_insight_owner_states.last_evaluated_at }) + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_user_id, user.id)); + const events = await db + .select({ eventType: cost_insight_events.event_type }) + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, user.id)); + + expect(config?.enabled).toBe(true); + expect(state?.lastEvaluatedAt).not.toBeNull(); + expect(events).toEqual([{ eventType: 'config_changed' }]); + }); + + it('rolls back settings, episode clearing, and event history together', async () => { + const user = await insertTestUser({ is_admin: true }); + await db.insert(cost_insight_owner_configs).values({ + owned_by_user_id: user.id, + spend_alerts_enabled: true, + spend_alerts_enabled_at: '2026-06-25T19:00:00.000Z', + }); + const [alertEvent] = await db + .insert(cost_insight_events) + .values({ + owned_by_user_id: user.id, + event_type: 'anomaly_alert', + alert_kind: 'anomaly', + title: 'Spend Anomaly Alert', + description: 'Usage-based spend is high.', + }) + .returning({ id: cost_insight_events.id }); + if (!alertEvent) throw new Error('Cost Insights alert fixture insert failed.'); + await db.insert(cost_insight_owner_states).values({ + owned_by_user_id: user.id, + active_anomaly_event_id: alertEvent.id, + active_anomaly_hour_start: '2026-06-25T19:00:00.000Z', + }); + + await expect( + updateCostInsightSettings(db, { + owner: { type: 'user', id: user.id }, + actorUserId: crypto.randomUUID(), + patch: { spendAlertsEnabled: false }, + }) + ).rejects.toThrow(); + + const [config] = await db + .select() + .from(cost_insight_owner_configs) + .where(eq(cost_insight_owner_configs.owned_by_user_id, user.id)); + const [state] = await db + .select() + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_user_id, user.id)); + const events = await db + .select({ eventType: cost_insight_events.event_type }) + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, user.id)); + + expect(config?.spend_alerts_enabled).toBe(true); + expect(state?.active_anomaly_event_id).toBe(alertEvent.id); + expect(events).toEqual([{ eventType: 'anomaly_alert' }]); }); it('turns off the threshold and clears the active threshold episode', async () => { @@ -381,8 +478,25 @@ describe('Cost Insights router', () => { if (!suggestion) throw new Error('Cost Insights suggestion fixture insert failed.'); const caller = await createCallerForUser(user.id); - await caller.costInsights.acknowledgeAlert({ alertKind: 'threshold_7d' }); - await caller.costInsights.acknowledgeAlert({ alertKind: 'threshold_7d' }); + await caller.costInsights.acknowledgeAlert({ + alertKind: 'threshold_7d', + eventId: crypto.randomUUID(), + }); + const [stateAfterStaleAcknowledgment] = await db + .select({ reviewedAt: cost_insight_owner_states.rolling_7_day_threshold_reviewed_at }) + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_user_id, user.id)); + expect(stateAfterStaleAcknowledgment?.reviewedAt).toBeNull(); + expect(trackingMock.trackCostInsightsAlertAction).not.toHaveBeenCalled(); + + await caller.costInsights.acknowledgeAlert({ + alertKind: 'threshold_7d', + eventId: alertEvent.id, + }); + await caller.costInsights.acknowledgeAlert({ + alertKind: 'threshold_7d', + eventId: alertEvent.id, + }); expect(trackingMock.trackCostInsightsAlertAction).toHaveBeenCalledTimes(1); expect(trackingMock.trackCostInsightsAlertAction).toHaveBeenCalledWith({ distinctId: user.id, @@ -405,6 +519,107 @@ describe('Cost Insights router', () => { suggestionKind: 'coding_plan', phase: 'accepted', }); + const actionEvents = await db + .select({ eventType: cost_insight_events.event_type }) + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, user.id)); + expect(actionEvents.filter(event => event.eventType === 'alert_reviewed')).toHaveLength(1); + expect(actionEvents.filter(event => event.eventType === 'suggestion_dismissed')).toHaveLength( + 1 + ); + }); + + it('rolls back alert acknowledgment when review event insertion fails', async () => { + const user = await insertTestUser({ is_admin: true }); + const [alertEvent] = await db + .insert(cost_insight_events) + .values({ + owned_by_user_id: user.id, + event_type: 'threshold_crossed', + alert_kind: 'threshold', + title: '24-hour Spend Threshold Alert', + description: 'Rolling 24-hour spend crossed threshold.', + }) + .returning({ id: cost_insight_events.id }); + if (!alertEvent) throw new Error('Cost Insights alert fixture insert failed.'); + await db.insert(cost_insight_owner_states).values({ + owned_by_user_id: user.id, + active_threshold_event_id: alertEvent.id, + threshold_crossing_active: true, + threshold_crossing_started_at: '2026-06-25T19:00:00.000Z', + }); + + await expect( + acknowledgeCostInsightAlert(db, { + owner: { type: 'user', id: user.id }, + alertKind: 'threshold', + eventId: alertEvent.id, + actorUserId: crypto.randomUUID(), + }) + ).rejects.toThrow(); + + const [state] = await db + .select({ reviewedAt: cost_insight_owner_states.threshold_reviewed_at }) + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_user_id, user.id)); + const events = await db + .select({ eventType: cost_insight_events.event_type }) + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, user.id)); + expect(state?.reviewedAt).toBeNull(); + expect(events).toEqual([{ eventType: 'threshold_crossed' }]); + }); + + it('rolls back suggestion dismissal when dismissal event insertion fails', async () => { + const user = await insertTestUser({ is_admin: true }); + const [suggestion] = await db + .insert(cost_insight_active_suggestions) + .values({ + owned_by_user_id: user.id, + suggestion_kind: 'kilo_pass', + suggestion_key: 'c'.repeat(64), + title: 'Review Kilo Pass coverage', + description: 'Kilo Pass may improve cost efficiency.', + cta_label: 'View Kilo Pass', + cta_href: '/subscriptions/kilo-pass', + evidence_window_start: '2026-06-18T19:00:00.000Z', + evidence_window_end: '2026-06-25T19:00:00.000Z', + observed_microdollars: 125_000_000, + benefit_label: 'Expert plan', + benefit_detail: '$199 + bonus credits', + }) + .returning({ id: cost_insight_active_suggestions.id }); + if (!suggestion) throw new Error('Cost Insights suggestion fixture insert failed.'); + + await expect( + db.transaction(async transaction => + costInsightRepositoryInternals.dismissCostInsightSuggestionInTransaction( + transaction, + { + owner: { type: 'user', id: user.id }, + suggestionId: suggestion.id, + actorUserId: user.id, + }, + async () => { + throw new Error('Injected event insertion failure.'); + } + ) + ) + ).rejects.toThrow('Injected event insertion failure.'); + + const [current] = await db + .select({ + dismissedAt: cost_insight_active_suggestions.dismissed_at, + dismissedByUserId: cost_insight_active_suggestions.dismissed_by_user_id, + }) + .from(cost_insight_active_suggestions) + .where(eq(cost_insight_active_suggestions.id, suggestion.id)); + const events = await db + .select({ eventType: cost_insight_events.event_type }) + .from(cost_insight_events) + .where(eq(cost_insight_events.owned_by_user_id, user.id)); + expect(current).toEqual({ dismissedAt: null, dismissedByUserId: null }); + expect(events).toEqual([]); }); it('paginates filtered event history beyond the first 50 rows', async () => { diff --git a/apps/web/src/routers/cost-insights-router.ts b/apps/web/src/routers/cost-insights-router.ts index 4429f2c607..4af63e8918 100644 --- a/apps/web/src/routers/cost-insights-router.ts +++ b/apps/web/src/routers/cost-insights-router.ts @@ -10,13 +10,14 @@ import { } from '@/lib/cost-insights/presenter'; import { acknowledgeCostInsightAlert, - clearCostInsightAlertState, - clearCostInsightAnomalyEpisode, clearCostInsightThresholdEpisode, createCostInsightEvent, - countOpenCostInsightReviewItems, + countUnreviewedCostInsightAlerts, dismissCostInsightSuggestion, + getCostInsightConfigChanges, + getCostInsightSettingsSnapshot, updateCostInsightOwnerConfig, + updateCostInsightSettings, } from '@/lib/cost-insights/repository'; import { evaluateCostInsightsForOwner } from '@/lib/cost-insights/evaluation'; import { parseSpendThresholdUsd } from '@/lib/cost-insights/policy'; @@ -45,6 +46,7 @@ const UpdateCostInsightsSettingsSchema = z.object({ const AcknowledgeCostInsightAlertSchema = z.object({ alertKind: z.enum(['anomaly', 'threshold', 'threshold_7d', 'threshold_30d']), + eventId: z.uuid(), }); const DismissCostInsightSuggestionSchema = z.object({ @@ -132,84 +134,6 @@ function trackSettingsSaved( }); } -function changedFields( - previous: { - spend_alerts_enabled: boolean; - anomaly_alerts_enabled: boolean; - cost_suggestions_enabled: boolean; - spend_threshold_microdollars: number | null; - spend_7_day_threshold_microdollars: number | null; - spend_30_day_threshold_microdollars: number | null; - }, - current: { - spend_alerts_enabled: boolean; - anomaly_alerts_enabled: boolean; - cost_suggestions_enabled: boolean; - spend_threshold_microdollars: number | null; - spend_7_day_threshold_microdollars: number | null; - spend_30_day_threshold_microdollars: number | null; - } -) { - const fields: Record = {}; - if (previous.spend_alerts_enabled !== current.spend_alerts_enabled) { - fields.spendAlertsEnabled = { - old: previous.spend_alerts_enabled, - new: current.spend_alerts_enabled, - }; - } - if (previous.anomaly_alerts_enabled !== current.anomaly_alerts_enabled) { - fields.anomalyAlertsEnabled = { - old: previous.anomaly_alerts_enabled, - new: current.anomaly_alerts_enabled, - }; - } - if (previous.cost_suggestions_enabled !== current.cost_suggestions_enabled) { - fields.costSuggestionsEnabled = { - old: previous.cost_suggestions_enabled, - new: current.cost_suggestions_enabled, - }; - } - if (previous.spend_threshold_microdollars !== current.spend_threshold_microdollars) { - fields.spendThresholdMicrodollars = { - old: previous.spend_threshold_microdollars, - new: current.spend_threshold_microdollars, - }; - } - if (previous.spend_7_day_threshold_microdollars !== current.spend_7_day_threshold_microdollars) { - fields.spend7DayThresholdMicrodollars = { - old: previous.spend_7_day_threshold_microdollars, - new: current.spend_7_day_threshold_microdollars, - }; - } - if ( - previous.spend_30_day_threshold_microdollars !== current.spend_30_day_threshold_microdollars - ) { - fields.spend30DayThresholdMicrodollars = { - old: previous.spend_30_day_threshold_microdollars, - new: current.spend_30_day_threshold_microdollars, - }; - } - return fields; -} - -function settingsSnapshot(config: { - spend_alerts_enabled: boolean; - anomaly_alerts_enabled: boolean; - cost_suggestions_enabled: boolean; - spend_threshold_microdollars: number | null; - spend_7_day_threshold_microdollars: number | null; - spend_30_day_threshold_microdollars: number | null; -}) { - return { - spendAlertsEnabled: config.spend_alerts_enabled, - anomalyAlertsEnabled: config.anomaly_alerts_enabled, - costSuggestionsEnabled: config.cost_suggestions_enabled, - spendThresholdMicrodollars: config.spend_threshold_microdollars, - spend7DayThresholdMicrodollars: config.spend_7_day_threshold_microdollars, - spend30DayThresholdMicrodollars: config.spend_30_day_threshold_microdollars, - }; -} - async function updateOwnerSettings(params: { owner: { type: 'user'; id: string } | { type: 'organization'; id: string }; actorUserId: string; @@ -230,65 +154,19 @@ async function updateOwnerSettings(params: { }); } - const { previous, current } = await updateCostInsightOwnerConfig(db, params.owner, { - spendAlertsEnabled: params.input.spendAlertsEnabled, - anomalyAlertsEnabled: params.input.anomalyAlertsEnabled, - costSuggestionsEnabled: params.input.costSuggestionsEnabled, - spendThresholdMicrodollars, - spend7DayThresholdMicrodollars, - spend30DayThresholdMicrodollars, + const { previous, current, hasChanges } = await updateCostInsightSettings(db, { + owner: params.owner, + actorUserId: params.actorUserId, + patch: { + spendAlertsEnabled: params.input.spendAlertsEnabled, + anomalyAlertsEnabled: params.input.anomalyAlertsEnabled, + costSuggestionsEnabled: params.input.costSuggestionsEnabled, + spendThresholdMicrodollars, + spend7DayThresholdMicrodollars, + spend30DayThresholdMicrodollars, + }, }); - const changes = changedFields(previous, current); - const hasChanges = Object.keys(changes).length > 0; - if (hasChanges && previous.spend_alerts_enabled && !current.spend_alerts_enabled) { - await clearCostInsightAlertState(db, params.owner); - await createCostInsightEvent(db, { - owner: params.owner, - eventType: 'disabled', - actorUserId: params.actorUserId, - title: 'Spend Alerts turned off', - description: 'Spend Alerts were disabled. Cost evidence remains visible.', - snapshot: { - changedFields: changes, - settings: settingsSnapshot(current), - }, - }); - } else if (hasChanges && (previous.spend_alerts_enabled || current.spend_alerts_enabled)) { - await createCostInsightEvent(db, { - owner: params.owner, - eventType: 'config_changed', - actorUserId: params.actorUserId, - title: 'Cost Insights settings changed', - description: 'Spend Alert settings were updated.', - snapshot: { - changedFields: changes, - settings: settingsSnapshot(current), - }, - }); - } - - if (previous.anomaly_alerts_enabled && !current.anomaly_alerts_enabled) { - await clearCostInsightAnomalyEpisode(db, params.owner); - } - if ( - previous.spend_threshold_microdollars !== null && - current.spend_threshold_microdollars === null - ) { - await clearCostInsightThresholdEpisode(db, params.owner, null, 'threshold'); - } - if ( - previous.spend_7_day_threshold_microdollars !== null && - current.spend_7_day_threshold_microdollars === null - ) { - await clearCostInsightThresholdEpisode(db, params.owner, null, 'threshold_7d'); - } - if ( - previous.spend_30_day_threshold_microdollars !== null && - current.spend_30_day_threshold_microdollars === null - ) { - await clearCostInsightThresholdEpisode(db, params.owner, null, 'threshold_30d'); - } if (current.spend_alerts_enabled) { await evaluateCostInsightsForOwner(db, params.owner); } @@ -321,8 +199,8 @@ async function disableOwnerThreshold(params: { title: 'Cost Insights settings changed', description: 'Spend threshold was turned off.', snapshot: { - changedFields: changedFields(previous, current), - settings: settingsSnapshot(current), + changedFields: getCostInsightConfigChanges(previous, current), + settings: getCostInsightSettingsSnapshot(current), }, }); } @@ -377,7 +255,7 @@ export const costInsightsRouter = createTRPCRouter({ }); }), getAttentionState: adminProcedure.query(async ({ ctx }) => { - const reviewItemCount = await countOpenCostInsightReviewItems(db, { + const reviewItemCount = await countUnreviewedCostInsightAlerts(db, { type: 'user', id: ctx.user.id, }); @@ -402,6 +280,7 @@ export const costInsightsRouter = createTRPCRouter({ const acknowledged = await acknowledgeCostInsightAlert(db, { owner: { type: 'user', id: ctx.user.id }, alertKind: input.alertKind, + eventId: input.eventId, actorUserId: ctx.user.id, }); if (acknowledged) { diff --git a/apps/web/src/routers/organizations/organization-cost-insights-router.test.ts b/apps/web/src/routers/organizations/organization-cost-insights-router.test.ts index 534767d7f1..5dfe842b73 100644 --- a/apps/web/src/routers/organizations/organization-cost-insights-router.test.ts +++ b/apps/web/src/routers/organizations/organization-cost-insights-router.test.ts @@ -1,4 +1,6 @@ import { jest } from '@jest/globals'; +import { cost_insight_events, cost_insight_owner_states } from '@kilocode/db/schema'; +import { eq } from 'drizzle-orm'; import { db } from '@/lib/drizzle'; import { @@ -17,6 +19,7 @@ jest.mock('@/lib/cost-insights/posthog-tracking', () => ({ })); const trackingMock: { + trackCostInsightsAlertAction: jest.Mock; trackCostInsightsSuggestionAction: jest.Mock; trackCostInsightsUiInteraction: jest.Mock; } = jest.requireMock('@/lib/cost-insights/posthog-tracking'); @@ -29,6 +32,7 @@ beforeAll(async () => { describe('Organization Cost Insights tracking', () => { beforeEach(() => { + trackingMock.trackCostInsightsAlertAction.mockClear(); trackingMock.trackCostInsightsSuggestionAction.mockClear(); trackingMock.trackCostInsightsUiInteraction.mockClear(); }); @@ -86,6 +90,52 @@ describe('Organization Cost Insights tracking', () => { }); }); + it('acknowledges only the displayed organization alert event', async () => { + const owner = await insertTestUser({ is_admin: true }); + const organization = await createOrganization('Cost Insights Review Org', owner.id); + const [alertEvent] = await db + .insert(cost_insight_events) + .values({ + owned_by_organization_id: organization.id, + event_type: 'anomaly_alert', + alert_kind: 'anomaly', + title: 'Spend Anomaly Alert', + description: 'Usage-based spend is high.', + }) + .returning({ id: cost_insight_events.id }); + if (!alertEvent) throw new Error('Cost Insights alert fixture insert failed.'); + await db.insert(cost_insight_owner_states).values({ + owned_by_organization_id: organization.id, + active_anomaly_event_id: alertEvent.id, + active_anomaly_hour_start: '2026-06-25T19:00:00.000Z', + }); + const caller = await createCallerForUser(owner.id); + + await caller.organizations.costInsights.acknowledgeAlert({ + organizationId: organization.id, + alertKind: 'anomaly', + eventId: crypto.randomUUID(), + }); + let [state] = await db + .select({ reviewedAt: cost_insight_owner_states.active_anomaly_reviewed_at }) + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_organization_id, organization.id)); + expect(state?.reviewedAt).toBeNull(); + expect(trackingMock.trackCostInsightsAlertAction).not.toHaveBeenCalled(); + + await caller.organizations.costInsights.acknowledgeAlert({ + organizationId: organization.id, + alertKind: 'anomaly', + eventId: alertEvent.id, + }); + [state] = await db + .select({ reviewedAt: cost_insight_owner_states.active_anomaly_reviewed_at }) + .from(cost_insight_owner_states) + .where(eq(cost_insight_owner_states.owned_by_organization_id, organization.id)); + expect(state?.reviewedAt).not.toBeNull(); + expect(trackingMock.trackCostInsightsAlertAction).toHaveBeenCalledTimes(1); + }); + it('limits notification access to admin owners and billing managers', async () => { const owner = await insertTestUser(); const adminBillingManager = await insertTestUser({ is_admin: true }); @@ -167,6 +217,7 @@ describe('Organization Cost Insights tracking', () => { caller.organizations.costInsights.acknowledgeAlert({ organizationId, alertKind: 'anomaly', + eventId: crypto.randomUUID(), }), () => caller.organizations.costInsights.disableThreshold({ organizationId }), () => diff --git a/apps/web/src/routers/organizations/organization-cost-insights-router.ts b/apps/web/src/routers/organizations/organization-cost-insights-router.ts index d54b4de245..bbbdab7cc8 100644 --- a/apps/web/src/routers/organizations/organization-cost-insights-router.ts +++ b/apps/web/src/routers/organizations/organization-cost-insights-router.ts @@ -11,7 +11,7 @@ import { } from '@/lib/cost-insights/presenter'; import { acknowledgeCostInsightAlert, - countOpenCostInsightReviewItems, + countUnreviewedCostInsightAlerts, dismissCostInsightSuggestion, } from '@/lib/cost-insights/repository'; import { @@ -170,7 +170,7 @@ export const organizationCostInsightsRouter = createTRPCRouter({ .input(OrganizationIdInputSchema) .query(async ({ ctx, input }) => { await resolveOrgReadContext(ctx, input.organizationId); - const reviewItemCount = await countOpenCostInsightReviewItems(db, { + const reviewItemCount = await countUnreviewedCostInsightAlerts(db, { type: 'organization', id: input.organizationId, }); @@ -201,6 +201,7 @@ export const organizationCostInsightsRouter = createTRPCRouter({ const acknowledged = await acknowledgeCostInsightAlert(db, { owner: { type: 'organization', id: input.organizationId }, alertKind: input.alertKind, + eventId: input.eventId, actorUserId: ctx.user.id, }); if (acknowledged) { diff --git a/apps/web/src/scripts/db/exa-usage-log-indexes.ts b/apps/web/src/scripts/db/exa-usage-log-indexes.ts index ba8d2c8a85..996ca904a9 100644 --- a/apps/web/src/scripts/db/exa-usage-log-indexes.ts +++ b/apps/web/src/scripts/db/exa-usage-log-indexes.ts @@ -1,7 +1,10 @@ import { setTimeout as sleep } from 'node:timers/promises'; import { db, type db as defaultDb } from '@/lib/drizzle'; -import { buildExaUsageLogPartitionIndexDefinitions } from '@/lib/exa-usage-partitions'; +import { + buildExaUsageLogPartitionIndexDefinitions, + buildExaUsageLogPartitionIndexDropStatement, +} from '@/lib/exa-usage-partitions'; import { sql } from 'drizzle-orm'; export type ExaUsageLogIndexScriptArgs = { @@ -17,6 +20,15 @@ type ExaUsageLogPartitionCatalogRow = { partition_name: string; }; +type ExaUsageLogIndexCatalogRow = { + schema_name: string; + index_name: string; + partition_schema_name: string; + partition_name: string; + is_valid: boolean; + is_ready: boolean; +}; + function usage(): string { return [ 'Usage:', @@ -106,6 +118,52 @@ export async function listExaUsageLogPartitions( return result.rows; } +async function getExaUsageLogIndexState( + fromDb: ExaUsageLogIndexDb, + schemaName: string, + indexName: string +): Promise { + const result = await fromDb.execute(sql` + SELECT + index_namespace.nspname AS schema_name, + index_class.relname AS index_name, + partition_namespace.nspname AS partition_schema_name, + partition_class.relname AS partition_name, + index_catalog.indisvalid AS is_valid, + index_catalog.indisready AS is_ready + FROM pg_catalog.pg_index AS index_catalog + INNER JOIN pg_catalog.pg_class AS index_class + ON index_class.oid = index_catalog.indexrelid + INNER JOIN pg_catalog.pg_namespace AS index_namespace + ON index_namespace.oid = index_class.relnamespace + INNER JOIN pg_catalog.pg_class AS partition_class + ON partition_class.oid = index_catalog.indrelid + INNER JOIN pg_catalog.pg_namespace AS partition_namespace + ON partition_namespace.oid = partition_class.relnamespace + WHERE index_namespace.nspname = ${schemaName} + AND index_class.relname = ${indexName} + `); + + const [indexState] = result.rows; + return indexState ?? null; +} + +function assertIndexTargetsPartition( + state: ExaUsageLogIndexCatalogRow, + schemaName: string, + partitionName: string +): void { + if ( + state.partition_schema_name !== schemaName || + state.partition_name !== partitionName || + state.schema_name !== schemaName + ) { + throw new Error( + `Index ${state.schema_name}.${state.index_name} targets ${state.partition_schema_name}.${state.partition_name}, expected ${schemaName}.${partitionName}.` + ); + } +} + export async function provisionHistoricalExaUsageLogIndexes( fromDb: ExaUsageLogIndexDb, options: ExaUsageLogIndexScriptArgs @@ -147,15 +205,48 @@ export async function provisionHistoricalExaUsageLogIndexes( } for (const index of plan.indexes) { + const initialState = await getExaUsageLogIndexState(fromDb, plan.schema_name, index.name); + if (initialState) { + assertIndexTargetsPartition(initialState, plan.schema_name, plan.partition_name); + } + + const needsRebuild = + initialState !== null && (!initialState.is_valid || !initialState.is_ready); + const needsCreate = initialState === null || needsRebuild; console.log( JSON.stringify({ mode: 'execute-index', schemaName: plan.schema_name, partitionName: plan.partition_name, indexName: index.name, + initialState: initialState + ? { valid: initialState.is_valid, ready: initialState.is_ready } + : null, + action: needsRebuild ? 'rebuild' : needsCreate ? 'create' : 'verify', }) ); - await fromDb.execute(sql.raw(index.statement)); + + if (needsRebuild) { + await fromDb.execute( + sql.raw(buildExaUsageLogPartitionIndexDropStatement(plan.schema_name, index.name)) + ); + } + if (needsCreate) { + await fromDb.execute(sql.raw(index.statement)); + } + + const finalState = await getExaUsageLogIndexState(fromDb, plan.schema_name, index.name); + if (!finalState) { + throw new Error( + `Index ${plan.schema_name}.${index.name} was not found after provisioning.` + ); + } + assertIndexTargetsPartition(finalState, plan.schema_name, plan.partition_name); + if (!finalState.is_valid || !finalState.is_ready) { + throw new Error( + `Index ${plan.schema_name}.${index.name} is not valid and ready after provisioning (indisvalid=${finalState.is_valid}, indisready=${finalState.is_ready}).` + ); + } } if (options.sleepMs > 0 && partitionIndex < plans.length - 1) { diff --git a/dev/seed/cost-insights/spend-evidence.ts b/dev/seed/cost-insights/spend-evidence.ts index 74aaa3dad8..d20b6f9402 100644 --- a/dev/seed/cost-insights/spend-evidence.ts +++ b/dev/seed/cost-insights/spend-evidence.ts @@ -1,6 +1,6 @@ import { createHash, randomUUID } from 'node:crypto'; -import { computeDatabaseUrl } from '@kilocode/db'; +import { and, computeDatabaseUrl, eq, inArray, like, lt, or, sql } from '@kilocode/db'; import { captureCostInsightSpend, COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, @@ -34,7 +34,6 @@ import { type CostInsightEventSnapshot, } from '@kilocode/db/schema'; import type { CodingPlanTermKind, GatewayApiKind } from '@kilocode/db/schema-types'; -import { and, eq, inArray, like, lt, or, sql } from 'drizzle-orm'; import { getSeedDb } from '../lib/db'; import { createSeedStripeCustomer, deleteSeedStripeCustomer } from '../lib/stripe'; @@ -56,6 +55,11 @@ type RollupMode = | 'repairable-drift' | 'unknown-taxonomy' | 'degraded-late'; +type CoverageMode = 'preserve' | 'disposable-full'; +type SpendEvidenceArgs = { + rollupMode: RollupMode; + coverageMode: CoverageMode; +}; const ROLLUP_MODES: RollupMode[] = [ 'bootstrap', 'healthy', @@ -82,7 +86,7 @@ const ORGANIZATION_OWNER: CostInsightSpendOwner = { const SEED_USER_IDS = [PERSONAL_OWNER_ID, BILLING_MANAGER_ID, ORGANIZATION_MEMBER_ID]; export const usage = - '[--rollup-mode ]'; + '[--rollup-mode ] [--coverage-mode ]'; type VariableDriver = { featureKey: string; @@ -183,27 +187,62 @@ function printUsage(): void { console.log(''); console.log('Rollup modes:'); console.log(' bootstrap Canonical history plus repairable drift; run backfill next.'); - console.log(' healthy Matching rollups and complete 90-day coverage.'); - console.log(' repairable-drift Complete coverage with missing, late, and stale rollups.'); + console.log(' healthy Matching rollups for 90 days of fixture evidence.'); + console.log(' repairable-drift Missing, late, and stale fixture rollups for repair tests.'); console.log(' unknown-taxonomy Healthy data plus one dry-run-only taxonomy diagnostic.'); - console.log(' degraded-late Late data plus unresolved degraded interval for inspection.'); + console.log( + ' degraded-late Late data plus unresolved interval; requires disposable-full coverage.' + ); + console.log(''); + console.log('Coverage modes:'); + console.log(' preserve Never modify global v1 coverage (default).'); + console.log( + ' disposable-full Replace global v1 coverage after verifying no unrelated evidence.' + ); console.log(''); - console.log('Default: bootstrap. Reruns replace only this fixture data and v1 coverage state.'); + console.log('Default: bootstrap with preserved global coverage.'); } -function parseRollupMode(args: string[]): RollupMode { - if (args.length === 0) return 'bootstrap'; - if (args.length !== 2 || args[0] !== '--rollup-mode') { - printUsage(); - throw new Error(`Unexpected arguments: ${args.join(' ')}`); - } - const requestedMode = args[1]; - const rollupMode = ROLLUP_MODES.find(mode => mode === requestedMode); - if (!rollupMode) { - printUsage(); - throw new Error(`Unknown rollup mode: ${requestedMode}`); +export function parseSpendEvidenceArgs(args: string[]): SpendEvidenceArgs { + let rollupMode: RollupMode = 'bootstrap'; + let coverageMode: CoverageMode = 'preserve'; + const seen = new Set(); + + for (let index = 0; index < args.length; index++) { + const flag = args[index]; + if (flag !== '--rollup-mode' && flag !== '--coverage-mode') { + printUsage(); + throw new Error(`Unexpected argument: ${flag}`); + } + if (seen.has(flag)) { + throw new Error(`Duplicate flag: ${flag}`); + } + seen.add(flag); + + const value = args[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for ${flag}`); + } + index++; + + if (flag === '--rollup-mode') { + const requestedMode = ROLLUP_MODES.find(mode => mode === value); + if (!requestedMode) { + printUsage(); + throw new Error(`Unknown rollup mode: ${value}`); + } + rollupMode = requestedMode; + continue; + } + + if (value !== 'preserve' && value !== 'disposable-full') { + printUsage(); + throw new Error(`Unknown coverage mode: ${value}`); + } + coverageMode = value; } - return rollupMode; + + return { rollupMode, coverageMode }; } function assertLocalDatabaseTarget(): { hostname: string; database: string; port: string } { @@ -226,6 +265,144 @@ function assertLocalDatabaseTarget(): { hostname: string; database: string; port }; } +type DisposableCoverageVerificationRow = { + unrelated_canonical_count: string; + unrelated_rollup_count: string; + unresolved_degraded_count: string; +}; + +type RollupCoverageRow = { + live_capture_start_hour: string; + coverage_start_hour: string | null; +}; + +export async function assertDisposableFullCoverageSafe( + database: Pick, 'execute'>, + startHour: string, + endHourExclusive: string +): Promise { + const result = await database.execute(sql` + WITH unrelated_canonical AS ( + SELECT 1 + FROM ${microdollar_usage} + WHERE ${microdollar_usage.created_at} >= ${startHour} + AND ${microdollar_usage.created_at} < ${endHourExclusive} + AND ${microdollar_usage.cost} > 0 + AND ( + (${microdollar_usage.organization_id} IS NULL + AND ${microdollar_usage.kilo_user_id} <> ${PERSONAL_OWNER_ID}) + OR (${microdollar_usage.organization_id} IS NOT NULL + AND ${microdollar_usage.organization_id} <> ${ORGANIZATION_ID}) + ) + UNION ALL + SELECT 1 + FROM ${exa_usage_log} + WHERE ${exa_usage_log.created_at} >= ${startHour} + AND ${exa_usage_log.created_at} < ${endHourExclusive} + AND ${exa_usage_log.charged_to_balance} = TRUE + AND ${exa_usage_log.cost_microdollars} > 0 + AND ( + (${exa_usage_log.organization_id} IS NULL + AND ${exa_usage_log.kilo_user_id} <> ${PERSONAL_OWNER_ID}) + OR (${exa_usage_log.organization_id} IS NOT NULL + AND ${exa_usage_log.organization_id} <> ${ORGANIZATION_ID}) + ) + UNION ALL + SELECT 1 + FROM ${coding_plan_terms} + INNER JOIN ${credit_transactions} + ON ${credit_transactions.id} = ${coding_plan_terms.credit_transaction_id} + WHERE ${credit_transactions.created_at} >= ${startHour} + AND ${credit_transactions.created_at} < ${endHourExclusive} + AND ${credit_transactions.amount_microdollars} < 0 + AND ( + (${credit_transactions.organization_id} IS NULL + AND ${credit_transactions.kilo_user_id} <> ${PERSONAL_OWNER_ID}) + OR (${credit_transactions.organization_id} IS NOT NULL + AND ${credit_transactions.organization_id} <> ${ORGANIZATION_ID}) + ) + UNION ALL + SELECT 1 + FROM ${credit_transactions} + WHERE ${credit_transactions.created_at} >= ${startHour} + AND ${credit_transactions.created_at} < ${endHourExclusive} + AND ${credit_transactions.amount_microdollars} < 0 + AND ( + ${credit_transactions.credit_category} LIKE 'kiloclaw-subscription:%' + OR ${credit_transactions.credit_category} LIKE 'kiloclaw-subscription-commit:%' + ) + AND ( + (${credit_transactions.organization_id} IS NULL + AND ${credit_transactions.kilo_user_id} <> ${PERSONAL_OWNER_ID}) + OR (${credit_transactions.organization_id} IS NOT NULL + AND ${credit_transactions.organization_id} <> ${ORGANIZATION_ID}) + ) + ), unrelated_rollups AS ( + SELECT 1 + FROM ${cost_insight_owner_hour_totals} + WHERE ${cost_insight_owner_hour_totals.hour_start} >= ${startHour} + AND ${cost_insight_owner_hour_totals.hour_start} < ${endHourExclusive} + AND ( + (${cost_insight_owner_hour_totals.owned_by_organization_id} IS NULL + AND ${cost_insight_owner_hour_totals.owned_by_user_id} <> ${PERSONAL_OWNER_ID}) + OR (${cost_insight_owner_hour_totals.owned_by_organization_id} IS NOT NULL + AND ${cost_insight_owner_hour_totals.owned_by_organization_id} <> ${ORGANIZATION_ID}) + ) + UNION ALL + SELECT 1 + FROM ${cost_insight_owner_hour_driver_buckets} + WHERE ${cost_insight_owner_hour_driver_buckets.hour_start} >= ${startHour} + AND ${cost_insight_owner_hour_driver_buckets.hour_start} < ${endHourExclusive} + AND ( + (${cost_insight_owner_hour_driver_buckets.owned_by_organization_id} IS NULL + AND ${cost_insight_owner_hour_driver_buckets.owned_by_user_id} <> ${PERSONAL_OWNER_ID}) + OR (${cost_insight_owner_hour_driver_buckets.owned_by_organization_id} IS NOT NULL + AND ${cost_insight_owner_hour_driver_buckets.owned_by_organization_id} <> ${ORGANIZATION_ID}) + ) + ) + SELECT + (SELECT COUNT(*)::text FROM unrelated_canonical) AS unrelated_canonical_count, + (SELECT COUNT(*)::text FROM unrelated_rollups) AS unrelated_rollup_count, + ( + SELECT COUNT(*)::text + FROM ${cost_insight_rollup_degraded_intervals} + WHERE ${cost_insight_rollup_degraded_intervals.resolved_at} IS NULL + AND ${cost_insight_rollup_degraded_intervals.start_hour} < ${endHourExclusive} + AND ${cost_insight_rollup_degraded_intervals.end_hour_exclusive} > ${startHour} + AND ${cost_insight_rollup_degraded_intervals.id} <> ${DEGRADED_INTERVAL_ID} + ) AS unresolved_degraded_count + `); + const verification = result.rows[0]; + if (!verification) { + throw new Error('Disposable full-coverage verification returned no result.'); + } + + const unrelatedCanonicalCount = Number(verification.unrelated_canonical_count); + const unrelatedRollupCount = Number(verification.unrelated_rollup_count); + const unresolvedDegradedCount = Number(verification.unresolved_degraded_count); + if (unrelatedCanonicalCount > 0 || unrelatedRollupCount > 0 || unresolvedDegradedCount > 0) { + throw new Error( + 'Refusing disposable-full coverage: found ' + + `${unrelatedCanonicalCount} unrelated canonical rows, ` + + `${unrelatedRollupCount} unrelated rollup rows, and ` + + `${unresolvedDegradedCount} unrelated unresolved degraded intervals in the fixture range.` + ); + } +} + +async function getRollupCoverage( + database: Pick, 'execute'> +): Promise { + const result = await database.execute(sql` + SELECT + ${cost_insight_rollup_coverage.live_capture_start_hour} AS live_capture_start_hour, + ${cost_insight_rollup_coverage.coverage_start_hour} AS coverage_start_hour + FROM ${cost_insight_rollup_coverage} + WHERE ${cost_insight_rollup_coverage.rollup_version} = 1 + `); + return result.rows[0] ?? null; +} + function floorUtcHour(timestamp: number): number { return Math.floor(timestamp / HOUR_MS) * HOUR_MS; } @@ -629,7 +806,7 @@ export async function run(...args: string[]): Promise { printUsage(); return; } - const rollupMode = parseRollupMode(args); + const { rollupMode, coverageMode } = parseSpendEvidenceArgs(args); const databaseTarget = assertLocalDatabaseTarget(); const db = getSeedDb(); @@ -842,8 +1019,11 @@ export async function run(...args: string[]): Promise { }, settings: { spendAlertsEnabled: true, + anomalyAlertsEnabled: true, costSuggestionsEnabled: true, spendThresholdMicrodollars: personalThresholdMicrodollars, + spend7DayThresholdMicrodollars: null, + spend30DayThresholdMicrodollars: null, }, }, dedupe_key: `dev-seed:personal:config:${currentHourIso}`, @@ -858,8 +1038,11 @@ export async function run(...args: string[]): Promise { snapshot: { settings: { spendAlertsEnabled: false, + anomalyAlertsEnabled: true, costSuggestionsEnabled: true, spendThresholdMicrodollars: personalThresholdMicrodollars, + spend7DayThresholdMicrodollars: null, + spend30DayThresholdMicrodollars: null, }, }, dedupe_key: `dev-seed:personal:disabled:${currentHourIso}`, @@ -942,8 +1125,11 @@ export async function run(...args: string[]): Promise { }, settings: { spendAlertsEnabled: true, + anomalyAlertsEnabled: true, costSuggestionsEnabled: true, spendThresholdMicrodollars: organizationThresholdMicrodollars, + spend7DayThresholdMicrodollars: null, + spend30DayThresholdMicrodollars: null, }, }, dedupe_key: `dev-seed:organization:config:${currentHourIso}`, @@ -955,11 +1141,35 @@ export async function run(...args: string[]): Promise { ...costInsightOwnerColumns(PERSONAL_OWNER), last_evaluated_at: currentHourIso, active_anomaly_event_id: personalAnomalyEventId, + active_anomaly_episode_id: personalAnomalyEventId, active_anomaly_hour_start: currentHourIso, + active_anomaly_snapshot: { + currentHourVariableMicrodollars: 14_000_000, + anomalyBaselineMicrodollars: 3_200_000, + anomalyThresholdMicrodollars: 10_000_000, + topDrivers: seedTopDrivers(PERSONAL_OWNER).filter( + driver => driver.spendCategory === 'variable' + ), + topDriversWindow: { + startInclusive: currentHourIso, + endExclusive: new Date(currentHour + 42 * 60 * 1_000).toISOString(), + spendCategory: 'variable', + }, + }, active_anomaly_reviewed_at: null, threshold_crossing_active: true, active_threshold_event_id: personalThresholdEventId, + active_threshold_episode_id: personalThresholdEventId, threshold_crossing_started_at: timestampAtHourOffset(currentHour, 1), + active_threshold_snapshot: { + rolling24HourMicrodollars: 62_500_000, + thresholdMicrodollars: personalThresholdMicrodollars, + topDrivers: seedTopDrivers(PERSONAL_OWNER), + topDriversWindow: { + startInclusive: new Date(currentHour - 24 * HOUR_MS + 42 * 60 * 1_000).toISOString(), + endExclusive: new Date(currentHour + 42 * 60 * 1_000).toISOString(), + }, + }, threshold_reviewed_at: null, threshold_recovered_at: null, }, @@ -967,11 +1177,35 @@ export async function run(...args: string[]): Promise { ...costInsightOwnerColumns(ORGANIZATION_OWNER), last_evaluated_at: currentHourIso, active_anomaly_event_id: organizationAnomalyEventId, + active_anomaly_episode_id: organizationAnomalyEventId, active_anomaly_hour_start: currentHourIso, + active_anomaly_snapshot: { + currentHourVariableMicrodollars: 46_000_000, + anomalyBaselineMicrodollars: 8_600_000, + anomalyThresholdMicrodollars: 25_800_000, + topDrivers: seedTopDrivers(ORGANIZATION_OWNER).filter( + driver => driver.spendCategory === 'variable' + ), + topDriversWindow: { + startInclusive: currentHourIso, + endExclusive: new Date(currentHour + 42 * 60 * 1_000).toISOString(), + spendCategory: 'variable', + }, + }, active_anomaly_reviewed_at: null, threshold_crossing_active: true, active_threshold_event_id: organizationThresholdEventId, + active_threshold_episode_id: organizationThresholdEventId, threshold_crossing_started_at: timestampAtHourOffset(currentHour, 1), + active_threshold_snapshot: { + rolling24HourMicrodollars: 128_000_000, + thresholdMicrodollars: organizationThresholdMicrodollars, + topDrivers: seedTopDrivers(ORGANIZATION_OWNER), + topDriversWindow: { + startInclusive: new Date(currentHour - 24 * HOUR_MS + 42 * 60 * 1_000).toISOString(), + endExclusive: new Date(currentHour + 42 * 60 * 1_000).toISOString(), + }, + }, threshold_reviewed_at: null, threshold_recovered_at: null, }, @@ -1018,6 +1252,30 @@ export async function run(...args: string[]): Promise { ]; const apiKinds = [...new Set(variableEvents.map(event => event.apiKind))]; + const existingCoverage = await getRollupCoverage(db); + if ( + coverageMode === 'preserve' && + existingCoverage && + (rollupMode === 'bootstrap' || rollupMode === 'repairable-drift') + ) { + throw new Error( + `Refusing ${rollupMode} fixture drift because global v1 coverage already exists. Use --rollup-mode healthy to preserve it, or use disposable-full coverage on a verified disposable database.` + ); + } + if (coverageMode === 'preserve' && rollupMode === 'degraded-late') { + throw new Error( + 'degraded-late requires --coverage-mode disposable-full because its unresolved interval is global.' + ); + } + + if (coverageMode === 'disposable-full') { + await assertDisposableFullCoverageSafe( + db, + coverageStartIso, + new Date(currentHour + HOUR_MS).toISOString() + ); + } + await ensureExaUsageLogPartitions( db, exaEvents.map(event => event.occurredAt) @@ -1127,9 +1385,11 @@ export async function run(...args: string[]): Promise { like(credit_transactions.credit_category, `coding-plan:${CREDIT_CATEGORY_PREFIX}:%`) ) ); - await tx - .delete(cost_insight_rollup_degraded_intervals) - .where(eq(cost_insight_rollup_degraded_intervals.id, DEGRADED_INTERVAL_ID)); + if (coverageMode === 'disposable-full') { + await tx + .delete(cost_insight_rollup_degraded_intervals) + .where(eq(cost_insight_rollup_degraded_intervals.id, DEGRADED_INTERVAL_ID)); + } const seedCostInsightEventIds = tx .select({ id: cost_insight_events.id }) @@ -1191,9 +1451,11 @@ export async function run(...args: string[]): Promise { eq(cost_insight_owner_hour_totals.owned_by_organization_id, ORGANIZATION_ID) ) ); - await tx - .delete(cost_insight_rollup_coverage) - .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); + if (coverageMode === 'disposable-full') { + await tx + .delete(cost_insight_rollup_coverage) + .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); + } for (const user of seedUsers) { await tx @@ -1646,13 +1908,15 @@ export async function run(...args: string[]): Promise { }); } - await tx.insert(cost_insight_rollup_coverage).values({ - rollup_version: 1, - live_capture_start_hour: currentHourIso, - coverage_start_hour: rollupMode === 'bootstrap' ? currentHourIso : coverageStartIso, - }); + if (coverageMode === 'disposable-full') { + await tx.insert(cost_insight_rollup_coverage).values({ + rollup_version: 1, + live_capture_start_hour: currentHourIso, + coverage_start_hour: rollupMode === 'bootstrap' ? currentHourIso : coverageStartIso, + }); + } - if (rollupMode === 'degraded-late') { + if (coverageMode === 'disposable-full' && rollupMode === 'degraded-late') { await tx.insert(cost_insight_rollup_degraded_intervals).values({ id: DEGRADED_INTERVAL_ID, start_hour: lateArrivalHourIso, @@ -1688,6 +1952,20 @@ export async function run(...args: string[]): Promise { throw error; } + const finalCoverage = await getRollupCoverage(db); + if (coverageMode === 'disposable-full') { + const expectedCoverageStart = rollupMode === 'bootstrap' ? currentHourIso : coverageStartIso; + if ( + !finalCoverage || + new Date(finalCoverage.live_capture_start_hour).toISOString() !== currentHourIso || + !finalCoverage.coverage_start_hour || + new Date(finalCoverage.coverage_start_hour).toISOString() !== expectedCoverageStart + ) { + await Promise.all(seedUsers.map(user => deleteSeedStripeCustomer(user.stripeCustomerId))); + throw new Error('Disposable full-coverage verification failed after fixture commit.'); + } + } + for (const user of seedUsers) { const previousStripeCustomerId = previousStripeCustomerIds.get(user.id); if ( @@ -1711,21 +1989,27 @@ export async function run(...args: string[]): Promise { '- Current-hour live capture plus missing history, late data, and one stale rollup.' ); } else if (rollupMode === 'repairable-drift') { - console.log('- Complete coverage with one missing rollup, late record, and stale rollup.'); + console.log('- One missing fixture rollup, late record, and stale fixture rollup.'); } else if (rollupMode === 'unknown-taxonomy') { console.log('- One unknown AI Gateway product taxonomy value for dry-run diagnostics.'); } else if (rollupMode === 'degraded-late') { console.log('- One late source row inside an unresolved degraded interval.'); } else { - console.log('- Matching hourly rollups with complete 90-day coverage.'); + console.log('- Matching hourly rollups for 90 days of fixture evidence.'); } console.log(''); console.log('Seed users have real Stripe test customers and support Stripe-backed pages.'); + if (coverageMode === 'preserve') { + console.log('Global v1 rollup coverage was preserved.'); + } else { + console.log('Global v1 rollup coverage was replaced after disposable-database verification.'); + } console.log('Use development fake login to open personal or organization Cost Insights.'); return { databaseTarget: `${databaseTarget.hostname}:${databaseTarget.port}/${databaseTarget.database}`, rollupMode, + coverageMode, executeSafe, personalOwnerId: PERSONAL_OWNER_ID, personalOwnerEmail: PERSONAL_OWNER_EMAIL, @@ -1749,7 +2033,9 @@ export async function run(...args: string[]): Promise { organizationMemberStripeCustomerId: seedUsers.find(user => user.id === ORGANIZATION_MEMBER_ID)?.stripeCustomerId ?? null, coverageStartHour: coverageStartIso, - rollupCoverageStartHour: rollupMode === 'bootstrap' ? currentHourIso : coverageStartIso, + rollupCoverageStartHour: finalCoverage?.coverage_start_hour + ? new Date(finalCoverage.coverage_start_hour).toISOString() + : null, currentHour: currentHourIso, maintenanceStartHour: rollupMode === 'bootstrap' ? coverageStartIso : maintenanceStartIso, maintenanceEndHour: currentHourIso, diff --git a/packages/db/src/cost-insights-rollups.ts b/packages/db/src/cost-insights-rollups.ts index 21d52a0f27..d5e57b767a 100644 --- a/packages/db/src/cost-insights-rollups.ts +++ b/packages/db/src/cost-insights-rollups.ts @@ -3,7 +3,11 @@ import { createHash } from 'node:crypto'; import { sql, type SQL } from 'drizzle-orm'; import type { WorkerDb } from './client'; -import { cost_insight_owner_hour_driver_buckets, cost_insight_owner_hour_totals } from './schema'; +import { + cost_insight_evaluation_dirty_owners, + cost_insight_owner_hour_driver_buckets, + cost_insight_owner_hour_totals, +} from './schema'; import { CostInsightSpendCategory, CostInsightSpendSource, @@ -257,6 +261,7 @@ type CostInsightCaptureOutcome = { function costInsightConflictTargets(owner: CostInsightSpendOwner): { total: SQL; driver: SQL; + dirtyOwner: SQL; } { return owner.type === 'user' ? { @@ -268,6 +273,10 @@ function costInsightConflictTargets(owner: CostInsightSpendOwner): { (owned_by_user_id, hour_start, spend_category, driver_key) WHERE owned_by_organization_id IS NULL `), + dirtyOwner: sql.raw(` + (owned_by_user_id) + WHERE owned_by_organization_id IS NULL + `), } : { total: sql.raw(` @@ -278,6 +287,10 @@ function costInsightConflictTargets(owner: CostInsightSpendOwner): { (owned_by_organization_id, hour_start, spend_category, driver_key) WHERE owned_by_user_id IS NULL `), + dirtyOwner: sql.raw(` + (owned_by_organization_id) + WHERE owned_by_user_id IS NULL + `), }; } @@ -388,9 +401,31 @@ async function writeCostInsightSpend( AND current_driver.provider_key = excluded.provider_key AND current_driver.actor_user_id = excluded.actor_user_id RETURNING 'ok'::text AS outcome + ), evaluation_dirty_upsert AS ( + INSERT INTO ${cost_insight_evaluation_dirty_owners} AS dirty_owner ( + owned_by_user_id, + owned_by_organization_id, + dirty_at, + next_attempt_at + ) + SELECT + capture_input.owned_by_user_id, + capture_input.owned_by_organization_id, + pg_catalog.clock_timestamp(), + pg_catalog.clock_timestamp() + FROM capture_input + CROSS JOIN driver_upsert + WHERE TRUE + ON CONFLICT ${conflictTargets.dirtyOwner} + DO UPDATE SET + generation = dirty_owner.generation + 1, + dirty_at = pg_catalog.clock_timestamp(), + next_attempt_at = LEAST(dirty_owner.next_attempt_at, pg_catalog.clock_timestamp()), + updated_at = pg_catalog.clock_timestamp() + RETURNING 'ok'::text AS outcome ) SELECT COALESCE( - (SELECT outcome FROM driver_upsert), + (SELECT outcome FROM evaluation_dirty_upsert), 'cost_insight_driver_digest_collision' ) AS outcome `); diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 008045ae3c..03c9d2a5d1 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -10,6 +10,7 @@ export { export { createDrizzleClient, type CreateDrizzleClientOptions, + type DrizzleClient, getWorkerDb, type GetWorkerDbOptions, type WorkerDb, @@ -77,4 +78,4 @@ export { type TerminalRenewalFailureRepository, type WaiveTerminalRenewalFailureInput, } from './kiloclaw-terminal-renewal-failure-repository'; -export { sql, ne } from 'drizzle-orm'; +export { and, eq, inArray, like, lt, ne, or, sql } from 'drizzle-orm'; diff --git a/packages/db/src/migrations/0174_sleepy_virginia_dare.sql b/packages/db/src/migrations/0174_bizarre_piledriver.sql similarity index 85% rename from packages/db/src/migrations/0174_sleepy_virginia_dare.sql rename to packages/db/src/migrations/0174_bizarre_piledriver.sql index e2a01edb36..610b52ac68 100644 --- a/packages/db/src/migrations/0174_sleepy_virginia_dare.sql +++ b/packages/db/src/migrations/0174_bizarre_piledriver.sql @@ -26,6 +26,23 @@ CREATE TABLE "cost_insight_active_suggestions" ( CONSTRAINT "cost_insight_active_suggestions_dismissed_by_check" CHECK ("cost_insight_active_suggestions"."dismissed_at" IS NOT NULL OR "cost_insight_active_suggestions"."dismissed_by_user_id" IS NULL) ); --> statement-breakpoint +CREATE TABLE "cost_insight_evaluation_dirty_owners" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "owned_by_user_id" text, + "owned_by_organization_id" uuid, + "generation" bigint DEFAULT '1' NOT NULL, + "dirty_at" timestamp with time zone DEFAULT now() NOT NULL, + "next_attempt_at" timestamp with time zone DEFAULT now() NOT NULL, + "claimed_at" timestamp with time zone, + "attempt_count" integer DEFAULT 0 NOT NULL, + "last_error_redacted" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_evaluation_dirty_owners_owner_check" CHECK (("cost_insight_evaluation_dirty_owners"."owned_by_user_id" IS NOT NULL AND "cost_insight_evaluation_dirty_owners"."owned_by_organization_id" IS NULL) OR ("cost_insight_evaluation_dirty_owners"."owned_by_user_id" IS NULL AND "cost_insight_evaluation_dirty_owners"."owned_by_organization_id" IS NOT NULL)), + CONSTRAINT "cost_insight_evaluation_dirty_owners_generation_check" CHECK ("cost_insight_evaluation_dirty_owners"."generation" > 0 AND "cost_insight_evaluation_dirty_owners"."generation" <= 9007199254740991), + CONSTRAINT "cost_insight_evaluation_dirty_owners_attempt_count_check" CHECK ("cost_insight_evaluation_dirty_owners"."attempt_count" >= 0) +); +--> statement-breakpoint CREATE TABLE "cost_insight_events" ( "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, "owned_by_user_id" text, @@ -150,30 +167,38 @@ CREATE TABLE "cost_insight_owner_states" ( "owned_by_organization_id" uuid, "last_evaluated_at" timestamp with time zone, "active_anomaly_event_id" uuid, + "active_anomaly_episode_id" uuid, "active_anomaly_hour_start" timestamp with time zone, + "active_anomaly_snapshot" jsonb, "active_anomaly_reviewed_at" timestamp with time zone, "threshold_crossing_active" boolean DEFAULT false NOT NULL, "active_threshold_event_id" uuid, + "active_threshold_episode_id" uuid, "threshold_crossing_started_at" timestamp with time zone, + "active_threshold_snapshot" jsonb, "threshold_reviewed_at" timestamp with time zone, "threshold_recovered_at" timestamp with time zone, "rolling_7_day_threshold_crossing_active" boolean DEFAULT false NOT NULL, "active_rolling_7_day_threshold_event_id" uuid, + "active_rolling_7_day_threshold_episode_id" uuid, "rolling_7_day_threshold_crossing_started_at" timestamp with time zone, + "active_rolling_7_day_threshold_snapshot" jsonb, "rolling_7_day_threshold_reviewed_at" timestamp with time zone, "rolling_7_day_threshold_recovered_at" timestamp with time zone, "rolling_30_day_threshold_crossing_active" boolean DEFAULT false NOT NULL, "active_rolling_30_day_threshold_event_id" uuid, + "active_rolling_30_day_threshold_episode_id" uuid, "rolling_30_day_threshold_crossing_started_at" timestamp with time zone, + "active_rolling_30_day_threshold_snapshot" jsonb, "rolling_30_day_threshold_reviewed_at" timestamp with time zone, "rolling_30_day_threshold_recovered_at" timestamp with time zone, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "cost_insight_owner_states_owner_check" CHECK (("cost_insight_owner_states"."owned_by_user_id" IS NOT NULL AND "cost_insight_owner_states"."owned_by_organization_id" IS NULL) OR ("cost_insight_owner_states"."owned_by_user_id" IS NULL AND "cost_insight_owner_states"."owned_by_organization_id" IS NOT NULL)), CONSTRAINT "cost_insight_owner_states_anomaly_hour_check" CHECK ("cost_insight_owner_states"."active_anomaly_hour_start" IS NULL OR "cost_insight_owner_states"."active_anomaly_hour_start" = date_trunc('hour', "cost_insight_owner_states"."active_anomaly_hour_start", 'UTC')), - CONSTRAINT "cost_insight_owner_states_threshold_active_check" CHECK ("cost_insight_owner_states"."threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_threshold_event_id" IS NULL AND "cost_insight_owner_states"."threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL)), - CONSTRAINT "cost_insight_owner_states_7_day_threshold_active_check" CHECK ("cost_insight_owner_states"."rolling_7_day_threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_event_id" IS NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL)), - CONSTRAINT "cost_insight_owner_states_30_day_threshold_active_check" CHECK ("cost_insight_owner_states"."rolling_30_day_threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_event_id" IS NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL)) + CONSTRAINT "cost_insight_owner_states_threshold_active_check" CHECK ("cost_insight_owner_states"."threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_threshold_event_id" IS NULL AND "cost_insight_owner_states"."active_threshold_episode_id" IS NULL AND "cost_insight_owner_states"."threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."active_threshold_snapshot" IS NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL)), + CONSTRAINT "cost_insight_owner_states_7_day_threshold_active_check" CHECK ("cost_insight_owner_states"."rolling_7_day_threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_event_id" IS NULL AND "cost_insight_owner_states"."active_rolling_7_day_threshold_episode_id" IS NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."active_rolling_7_day_threshold_snapshot" IS NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL)), + CONSTRAINT "cost_insight_owner_states_30_day_threshold_active_check" CHECK ("cost_insight_owner_states"."rolling_30_day_threshold_crossing_active" = TRUE OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_event_id" IS NULL AND "cost_insight_owner_states"."active_rolling_30_day_threshold_episode_id" IS NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_crossing_started_at" IS NULL AND "cost_insight_owner_states"."active_rolling_30_day_threshold_snapshot" IS NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL)) ); --> statement-breakpoint CREATE TABLE "cost_insight_rollup_coverage" ( @@ -210,6 +235,8 @@ CREATE TABLE "cost_insight_rollup_degraded_intervals" ( ALTER TABLE "cost_insight_active_suggestions" ADD CONSTRAINT "cost_insight_active_suggestions_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "cost_insight_active_suggestions" ADD CONSTRAINT "cost_insight_active_suggestions_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "cost_insight_active_suggestions" ADD CONSTRAINT "cost_insight_active_suggestions_dismissed_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("dismissed_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cost_insight_evaluation_dirty_owners" ADD CONSTRAINT "cost_insight_evaluation_dirty_owners_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "cost_insight_evaluation_dirty_owners" ADD CONSTRAINT "cost_insight_evaluation_dirty_owners_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "cost_insight_events" ADD CONSTRAINT "cost_insight_events_owned_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("owned_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "cost_insight_events" ADD CONSTRAINT "cost_insight_events_owned_by_organization_id_organizations_id_fk" FOREIGN KEY ("owned_by_organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "cost_insight_events" ADD CONSTRAINT "cost_insight_events_active_suggestion_id_cost_insight_active_suggestions_id_fk" FOREIGN KEY ("active_suggestion_id") REFERENCES "public"."cost_insight_active_suggestions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint @@ -233,6 +260,9 @@ CREATE UNIQUE INDEX "UQ_cost_insight_active_suggestions_user_key" ON "cost_insig CREATE UNIQUE INDEX "UQ_cost_insight_active_suggestions_org_key" ON "cost_insight_active_suggestions" USING btree ("owned_by_organization_id","suggestion_key") WHERE "cost_insight_active_suggestions"."owned_by_user_id" is null;--> statement-breakpoint CREATE INDEX "IDX_cost_insight_active_suggestions_user_active" ON "cost_insight_active_suggestions" USING btree ("owned_by_user_id","created_at" DESC NULLS LAST) WHERE "cost_insight_active_suggestions"."owned_by_user_id" IS NOT NULL AND "cost_insight_active_suggestions"."dismissed_at" IS NULL;--> statement-breakpoint CREATE INDEX "IDX_cost_insight_active_suggestions_org_active" ON "cost_insight_active_suggestions" USING btree ("owned_by_organization_id","created_at" DESC NULLS LAST) WHERE "cost_insight_active_suggestions"."owned_by_organization_id" IS NOT NULL AND "cost_insight_active_suggestions"."dismissed_at" IS NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_evaluation_dirty_owners_user" ON "cost_insight_evaluation_dirty_owners" USING btree ("owned_by_user_id") WHERE "cost_insight_evaluation_dirty_owners"."owned_by_organization_id" is null;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_cost_insight_evaluation_dirty_owners_org" ON "cost_insight_evaluation_dirty_owners" USING btree ("owned_by_organization_id") WHERE "cost_insight_evaluation_dirty_owners"."owned_by_user_id" is null;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_evaluation_dirty_owners_claim" ON "cost_insight_evaluation_dirty_owners" USING btree ("next_attempt_at","claimed_at","dirty_at","id");--> statement-breakpoint CREATE INDEX "IDX_cost_insight_events_user_occurred" ON "cost_insight_events" USING btree ("owned_by_user_id","occurred_at" DESC NULLS LAST,"id");--> statement-breakpoint CREATE INDEX "IDX_cost_insight_events_org_occurred" ON "cost_insight_events" USING btree ("owned_by_organization_id","occurred_at" DESC NULLS LAST,"id");--> statement-breakpoint CREATE INDEX "IDX_cost_insight_events_occurred" ON "cost_insight_events" USING btree ("occurred_at");--> statement-breakpoint @@ -252,6 +282,6 @@ CREATE UNIQUE INDEX "UQ_cost_insight_owner_hour_totals_org" ON "cost_insight_own CREATE INDEX "IDX_cost_insight_owner_hour_totals_hour" ON "cost_insight_owner_hour_totals" USING btree ("hour_start");--> statement-breakpoint CREATE UNIQUE INDEX "UQ_cost_insight_owner_states_user" ON "cost_insight_owner_states" USING btree ("owned_by_user_id") WHERE "cost_insight_owner_states"."owned_by_organization_id" is null;--> statement-breakpoint CREATE UNIQUE INDEX "UQ_cost_insight_owner_states_org" ON "cost_insight_owner_states" USING btree ("owned_by_organization_id") WHERE "cost_insight_owner_states"."owned_by_user_id" is null;--> statement-breakpoint -CREATE INDEX "IDX_cost_insight_owner_states_unreviewed_user" ON "cost_insight_owner_states" USING btree ("owned_by_user_id","updated_at") WHERE "cost_insight_owner_states"."owned_by_user_id" IS NOT NULL AND (("cost_insight_owner_states"."active_anomaly_event_id" IS NOT NULL AND "cost_insight_owner_states"."active_anomaly_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL));--> statement-breakpoint -CREATE INDEX "IDX_cost_insight_owner_states_unreviewed_org" ON "cost_insight_owner_states" USING btree ("owned_by_organization_id","updated_at") WHERE "cost_insight_owner_states"."owned_by_organization_id" IS NOT NULL AND (("cost_insight_owner_states"."active_anomaly_event_id" IS NOT NULL AND "cost_insight_owner_states"."active_anomaly_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_event_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL));--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_owner_states_unreviewed_user" ON "cost_insight_owner_states" USING btree ("owned_by_user_id","updated_at") WHERE "cost_insight_owner_states"."owned_by_user_id" IS NOT NULL AND (("cost_insight_owner_states"."active_anomaly_episode_id" IS NOT NULL AND "cost_insight_owner_states"."active_anomaly_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL));--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_owner_states_unreviewed_org" ON "cost_insight_owner_states" USING btree ("owned_by_organization_id","updated_at") WHERE "cost_insight_owner_states"."owned_by_organization_id" IS NOT NULL AND (("cost_insight_owner_states"."active_anomaly_episode_id" IS NOT NULL AND "cost_insight_owner_states"."active_anomaly_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL));--> statement-breakpoint CREATE INDEX "IDX_cost_insight_degraded_intervals_unresolved" ON "cost_insight_rollup_degraded_intervals" USING btree ("start_hour","end_hour_exclusive") WHERE "cost_insight_rollup_degraded_intervals"."resolved_at" is null; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0174_snapshot.json b/packages/db/src/migrations/meta/0174_snapshot.json index 72e267f4c9..64e0c55666 100644 --- a/packages/db/src/migrations/meta/0174_snapshot.json +++ b/packages/db/src/migrations/meta/0174_snapshot.json @@ -1,5 +1,5 @@ { - "id": "e22596a2-8774-48f3-a63f-792566ac90c0", + "id": "f1321a3d-546a-452a-bcd8-b1361304f467", "prevId": "06265fd6-428e-4b7d-9092-dba307d6255e", "version": "7", "dialect": "postgresql", @@ -8516,6 +8516,198 @@ }, "isRLSEnabled": false }, + "public.cost_insight_evaluation_dirty_owners": { + "name": "cost_insight_evaluation_dirty_owners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "dirty_at": { + "name": "dirty_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cost_insight_evaluation_dirty_owners_user": { + "name": "UQ_cost_insight_evaluation_dirty_owners_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_evaluation_dirty_owners\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_evaluation_dirty_owners_org": { + "name": "UQ_cost_insight_evaluation_dirty_owners_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_evaluation_dirty_owners\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_evaluation_dirty_owners_claim": { + "name": "IDX_cost_insight_evaluation_dirty_owners_claim", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dirty_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_evaluation_dirty_owners_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_evaluation_dirty_owners_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_evaluation_dirty_owners", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_evaluation_dirty_owners_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_evaluation_dirty_owners_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_evaluation_dirty_owners", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_evaluation_dirty_owners_owner_check": { + "name": "cost_insight_evaluation_dirty_owners_owner_check", + "value": "(\"cost_insight_evaluation_dirty_owners\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_evaluation_dirty_owners\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_evaluation_dirty_owners\".\"owned_by_user_id\" IS NULL AND \"cost_insight_evaluation_dirty_owners\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_evaluation_dirty_owners_generation_check": { + "name": "cost_insight_evaluation_dirty_owners_generation_check", + "value": "\"cost_insight_evaluation_dirty_owners\".\"generation\" > 0 AND \"cost_insight_evaluation_dirty_owners\".\"generation\" <= 9007199254740991" + }, + "cost_insight_evaluation_dirty_owners_attempt_count_check": { + "name": "cost_insight_evaluation_dirty_owners_attempt_count_check", + "value": "\"cost_insight_evaluation_dirty_owners\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, "public.cost_insight_events": { "name": "cost_insight_events", "schema": "", @@ -9753,12 +9945,24 @@ "primaryKey": false, "notNull": false }, + "active_anomaly_episode_id": { + "name": "active_anomaly_episode_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "active_anomaly_hour_start": { "name": "active_anomaly_hour_start", "type": "timestamp with time zone", "primaryKey": false, "notNull": false }, + "active_anomaly_snapshot": { + "name": "active_anomaly_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, "active_anomaly_reviewed_at": { "name": "active_anomaly_reviewed_at", "type": "timestamp with time zone", @@ -9778,12 +9982,24 @@ "primaryKey": false, "notNull": false }, + "active_threshold_episode_id": { + "name": "active_threshold_episode_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "threshold_crossing_started_at": { "name": "threshold_crossing_started_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": false }, + "active_threshold_snapshot": { + "name": "active_threshold_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, "threshold_reviewed_at": { "name": "threshold_reviewed_at", "type": "timestamp with time zone", @@ -9809,12 +10025,24 @@ "primaryKey": false, "notNull": false }, + "active_rolling_7_day_threshold_episode_id": { + "name": "active_rolling_7_day_threshold_episode_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "rolling_7_day_threshold_crossing_started_at": { "name": "rolling_7_day_threshold_crossing_started_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": false }, + "active_rolling_7_day_threshold_snapshot": { + "name": "active_rolling_7_day_threshold_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, "rolling_7_day_threshold_reviewed_at": { "name": "rolling_7_day_threshold_reviewed_at", "type": "timestamp with time zone", @@ -9840,12 +10068,24 @@ "primaryKey": false, "notNull": false }, + "active_rolling_30_day_threshold_episode_id": { + "name": "active_rolling_30_day_threshold_episode_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "rolling_30_day_threshold_crossing_started_at": { "name": "rolling_30_day_threshold_crossing_started_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": false }, + "active_rolling_30_day_threshold_snapshot": { + "name": "active_rolling_30_day_threshold_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, "rolling_30_day_threshold_reviewed_at": { "name": "rolling_30_day_threshold_reviewed_at", "type": "timestamp with time zone", @@ -9923,7 +10163,7 @@ } ], "isUnique": false, - "where": "\"cost_insight_owner_states\".\"owned_by_user_id\" IS NOT NULL AND ((\"cost_insight_owner_states\".\"active_anomaly_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"active_anomaly_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL))", + "where": "\"cost_insight_owner_states\".\"owned_by_user_id\" IS NOT NULL AND ((\"cost_insight_owner_states\".\"active_anomaly_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"active_anomaly_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL))", "concurrently": false, "method": "btree", "with": {} @@ -9945,7 +10185,7 @@ } ], "isUnique": false, - "where": "\"cost_insight_owner_states\".\"owned_by_organization_id\" IS NOT NULL AND ((\"cost_insight_owner_states\".\"active_anomaly_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"active_anomaly_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_event_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL))", + "where": "\"cost_insight_owner_states\".\"owned_by_organization_id\" IS NOT NULL AND ((\"cost_insight_owner_states\".\"active_anomaly_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"active_anomaly_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL))", "concurrently": false, "method": "btree", "with": {} @@ -10045,15 +10285,15 @@ }, "cost_insight_owner_states_threshold_active_check": { "name": "cost_insight_owner_states_threshold_active_check", - "value": "\"cost_insight_owner_states\".\"threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL)" + "value": "\"cost_insight_owner_states\".\"threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"active_threshold_episode_id\" IS NULL AND \"cost_insight_owner_states\".\"threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"active_threshold_snapshot\" IS NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL)" }, "cost_insight_owner_states_7_day_threshold_active_check": { "name": "cost_insight_owner_states_7_day_threshold_active_check", - "value": "\"cost_insight_owner_states\".\"rolling_7_day_threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL)" + "value": "\"cost_insight_owner_states\".\"rolling_7_day_threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"active_rolling_7_day_threshold_episode_id\" IS NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"active_rolling_7_day_threshold_snapshot\" IS NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL)" }, "cost_insight_owner_states_30_day_threshold_active_check": { "name": "cost_insight_owner_states_30_day_threshold_active_check", - "value": "\"cost_insight_owner_states\".\"rolling_30_day_threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL)" + "value": "\"cost_insight_owner_states\".\"rolling_30_day_threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"active_rolling_30_day_threshold_episode_id\" IS NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"active_rolling_30_day_threshold_snapshot\" IS NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL)" } }, "isRLSEnabled": false diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index cd21e35127..d4a69a72f1 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1223,8 +1223,8 @@ { "idx": 174, "version": "7", - "when": 1782477973153, - "tag": "0174_sleepy_virginia_dare", + "when": 1782500059599, + "tag": "0174_bizarre_piledriver", "breakpoints": true } ] diff --git a/packages/db/src/schema.test.ts b/packages/db/src/schema.test.ts index 0c5d135cff..d345ea8a7b 100644 --- a/packages/db/src/schema.test.ts +++ b/packages/db/src/schema.test.ts @@ -551,7 +551,7 @@ describe('database schema', () => { ], CostInsightAlertKind: ['anomaly', 'threshold', 'threshold_7d', 'threshold_30d'], CostInsightSuggestionKind: ['coding_plan', 'kilo_pass'], - CostInsightNotificationStatus: ['pending', 'sending', 'sent', 'failed'], + CostInsightNotificationStatus: ['pending', 'sending', 'sent', 'failed', 'skipped'], CodeReviewAnalyticsCaptureStatus: ['captured', 'missing', 'invalid', 'omitted'], CodeReviewAnalyticsChangeType: [ 'bug_fix', diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 1f6bde520c..c4bbd7a95e 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -2845,6 +2845,66 @@ export const cost_insight_owner_hour_driver_buckets = pgTable( export type CostInsightOwnerHourDriverBucket = typeof cost_insight_owner_hour_driver_buckets.$inferSelect; +export const cost_insight_evaluation_dirty_owners = pgTable( + 'cost_insight_evaluation_dirty_owners', + { + id: uuid() + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey() + .notNull(), + owned_by_user_id: text().references(() => kilocode_users.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + owned_by_organization_id: uuid().references(() => organizations.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + generation: bigint({ mode: 'number' }) + .default(sql`'1'`) + .notNull(), + dirty_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + next_attempt_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + claimed_at: timestamp({ withTimezone: true, mode: 'string' }), + attempt_count: integer().default(0).notNull(), + last_error_redacted: text(), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + uniqueIndex('UQ_cost_insight_evaluation_dirty_owners_user') + .on(table.owned_by_user_id) + .where(isNull(table.owned_by_organization_id)), + uniqueIndex('UQ_cost_insight_evaluation_dirty_owners_org') + .on(table.owned_by_organization_id) + .where(isNull(table.owned_by_user_id)), + index('IDX_cost_insight_evaluation_dirty_owners_claim').on( + table.next_attempt_at, + table.claimed_at, + table.dirty_at, + table.id + ), + check( + 'cost_insight_evaluation_dirty_owners_owner_check', + sql`(${table.owned_by_user_id} IS NOT NULL AND ${table.owned_by_organization_id} IS NULL) OR (${table.owned_by_user_id} IS NULL AND ${table.owned_by_organization_id} IS NOT NULL)` + ), + check( + 'cost_insight_evaluation_dirty_owners_generation_check', + sql`${table.generation} > 0 AND ${table.generation} <= 9007199254740991` + ), + check( + 'cost_insight_evaluation_dirty_owners_attempt_count_check', + sql`${table.attempt_count} >= 0` + ), + ] +); + +export type CostInsightEvaluationDirtyOwner = + typeof cost_insight_evaluation_dirty_owners.$inferSelect; + export const cost_insight_rollup_coverage = pgTable( 'cost_insight_rollup_coverage', { @@ -3222,33 +3282,41 @@ export const cost_insight_owner_states = pgTable( active_anomaly_event_id: uuid().references(() => cost_insight_events.id, { onDelete: 'set null', }), + active_anomaly_episode_id: uuid(), active_anomaly_hour_start: timestamp({ withTimezone: true, mode: 'string' }), + active_anomaly_snapshot: jsonb().$type(), active_anomaly_reviewed_at: timestamp({ withTimezone: true, mode: 'string' }), threshold_crossing_active: boolean().default(false).notNull(), active_threshold_event_id: uuid().references(() => cost_insight_events.id, { onDelete: 'set null', }), + active_threshold_episode_id: uuid(), threshold_crossing_started_at: timestamp({ withTimezone: true, mode: 'string' }), + active_threshold_snapshot: jsonb().$type(), threshold_reviewed_at: timestamp({ withTimezone: true, mode: 'string' }), threshold_recovered_at: timestamp({ withTimezone: true, mode: 'string' }), rolling_7_day_threshold_crossing_active: boolean().default(false).notNull(), active_rolling_7_day_threshold_event_id: uuid().references(() => cost_insight_events.id, { onDelete: 'set null', }), + active_rolling_7_day_threshold_episode_id: uuid(), rolling_7_day_threshold_crossing_started_at: timestamp({ withTimezone: true, mode: 'string', }), + active_rolling_7_day_threshold_snapshot: jsonb().$type(), rolling_7_day_threshold_reviewed_at: timestamp({ withTimezone: true, mode: 'string' }), rolling_7_day_threshold_recovered_at: timestamp({ withTimezone: true, mode: 'string' }), rolling_30_day_threshold_crossing_active: boolean().default(false).notNull(), active_rolling_30_day_threshold_event_id: uuid().references(() => cost_insight_events.id, { onDelete: 'set null', }), + active_rolling_30_day_threshold_episode_id: uuid(), rolling_30_day_threshold_crossing_started_at: timestamp({ withTimezone: true, mode: 'string', }), + active_rolling_30_day_threshold_snapshot: jsonb().$type(), rolling_30_day_threshold_reviewed_at: timestamp({ withTimezone: true, mode: 'string' }), rolling_30_day_threshold_recovered_at: timestamp({ withTimezone: true, mode: 'string' }), created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), @@ -3267,12 +3335,12 @@ export const cost_insight_owner_states = pgTable( index('IDX_cost_insight_owner_states_unreviewed_user') .on(table.owned_by_user_id, table.updated_at) .where( - sql`${table.owned_by_user_id} IS NOT NULL AND ((${table.active_anomaly_event_id} IS NOT NULL AND ${table.active_anomaly_reviewed_at} IS NULL) OR (${table.active_threshold_event_id} IS NOT NULL AND ${table.threshold_reviewed_at} IS NULL) OR (${table.active_rolling_7_day_threshold_event_id} IS NOT NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL) OR (${table.active_rolling_30_day_threshold_event_id} IS NOT NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL))` + sql`${table.owned_by_user_id} IS NOT NULL AND ((${table.active_anomaly_episode_id} IS NOT NULL AND ${table.active_anomaly_reviewed_at} IS NULL) OR (${table.active_threshold_episode_id} IS NOT NULL AND ${table.threshold_reviewed_at} IS NULL) OR (${table.active_rolling_7_day_threshold_episode_id} IS NOT NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL) OR (${table.active_rolling_30_day_threshold_episode_id} IS NOT NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL))` ), index('IDX_cost_insight_owner_states_unreviewed_org') .on(table.owned_by_organization_id, table.updated_at) .where( - sql`${table.owned_by_organization_id} IS NOT NULL AND ((${table.active_anomaly_event_id} IS NOT NULL AND ${table.active_anomaly_reviewed_at} IS NULL) OR (${table.active_threshold_event_id} IS NOT NULL AND ${table.threshold_reviewed_at} IS NULL) OR (${table.active_rolling_7_day_threshold_event_id} IS NOT NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL) OR (${table.active_rolling_30_day_threshold_event_id} IS NOT NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL))` + sql`${table.owned_by_organization_id} IS NOT NULL AND ((${table.active_anomaly_episode_id} IS NOT NULL AND ${table.active_anomaly_reviewed_at} IS NULL) OR (${table.active_threshold_episode_id} IS NOT NULL AND ${table.threshold_reviewed_at} IS NULL) OR (${table.active_rolling_7_day_threshold_episode_id} IS NOT NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL) OR (${table.active_rolling_30_day_threshold_episode_id} IS NOT NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL))` ), check( 'cost_insight_owner_states_owner_check', @@ -3284,15 +3352,15 @@ export const cost_insight_owner_states = pgTable( ), check( 'cost_insight_owner_states_threshold_active_check', - sql`${table.threshold_crossing_active} = TRUE OR (${table.active_threshold_event_id} IS NULL AND ${table.threshold_crossing_started_at} IS NULL AND ${table.threshold_reviewed_at} IS NULL)` + sql`${table.threshold_crossing_active} = TRUE OR (${table.active_threshold_event_id} IS NULL AND ${table.active_threshold_episode_id} IS NULL AND ${table.threshold_crossing_started_at} IS NULL AND ${table.active_threshold_snapshot} IS NULL AND ${table.threshold_reviewed_at} IS NULL)` ), check( 'cost_insight_owner_states_7_day_threshold_active_check', - sql`${table.rolling_7_day_threshold_crossing_active} = TRUE OR (${table.active_rolling_7_day_threshold_event_id} IS NULL AND ${table.rolling_7_day_threshold_crossing_started_at} IS NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL)` + sql`${table.rolling_7_day_threshold_crossing_active} = TRUE OR (${table.active_rolling_7_day_threshold_event_id} IS NULL AND ${table.active_rolling_7_day_threshold_episode_id} IS NULL AND ${table.rolling_7_day_threshold_crossing_started_at} IS NULL AND ${table.active_rolling_7_day_threshold_snapshot} IS NULL AND ${table.rolling_7_day_threshold_reviewed_at} IS NULL)` ), check( 'cost_insight_owner_states_30_day_threshold_active_check', - sql`${table.rolling_30_day_threshold_crossing_active} = TRUE OR (${table.active_rolling_30_day_threshold_event_id} IS NULL AND ${table.rolling_30_day_threshold_crossing_started_at} IS NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL)` + sql`${table.rolling_30_day_threshold_crossing_active} = TRUE OR (${table.active_rolling_30_day_threshold_event_id} IS NULL AND ${table.active_rolling_30_day_threshold_episode_id} IS NULL AND ${table.rolling_30_day_threshold_crossing_started_at} IS NULL AND ${table.active_rolling_30_day_threshold_snapshot} IS NULL AND ${table.rolling_30_day_threshold_reviewed_at} IS NULL)` ), ] ); From 2d48162faf53bd3f458f0b9aaf1d1be9aac66eb5 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Fri, 26 Jun 2026 22:56:56 +0200 Subject: [PATCH 11/11] fix(cost-insights): harden hourly evaluation --- .../cron/cost-insights-hourly/route.test.ts | 10 +- .../api/cron/cost-insights-hourly/route.ts | 5 + .../cost-insights/canonical-sources.test.ts | 21 ++ .../lib/cost-insights/canonical-sources.ts | 24 +- apps/web/src/lib/cost-insights/evaluation.ts | 57 ++-- .../cost-insights/hourly-sweep-repository.ts | 133 +++++++++ apps/web/src/lib/cost-insights/jobs.ts | 165 +++++++++-- .../notifications.integration.test.ts | 36 ++- .../src/lib/cost-insights/notifications.ts | 52 ++-- apps/web/src/lib/cost-insights/repository.ts | 128 ++++++-- .../spend-repository.integration.test.ts | 274 ++++++++++++++++++ .../src/lib/cost-insights/spend-repository.ts | 274 +++++++++++++++++- .../src/cost-insight-event-snapshot.test.ts | 95 ++++++ packages/db/src/cost-insights-rollups.test.ts | 62 ++++ packages/db/src/cost-insights-rollups.ts | 2 +- ...piledriver.sql => 0174_minor_the_call.sql} | 30 +- .../db/src/migrations/meta/0174_snapshot.json | 189 +++++++++++- packages/db/src/migrations/meta/_journal.json | 4 +- packages/db/src/schema.ts | 176 ++++++++--- 19 files changed, 1583 insertions(+), 154 deletions(-) create mode 100644 apps/web/src/lib/cost-insights/hourly-sweep-repository.ts create mode 100644 packages/db/src/cost-insight-event-snapshot.test.ts rename packages/db/src/migrations/{0174_bizarre_piledriver.sql => 0174_minor_the_call.sql} (90%) diff --git a/apps/web/src/app/api/cron/cost-insights-hourly/route.test.ts b/apps/web/src/app/api/cron/cost-insights-hourly/route.test.ts index 15b6bd6ff7..9fa6ce5eda 100644 --- a/apps/web/src/app/api/cron/cost-insights-hourly/route.test.ts +++ b/apps/web/src/app/api/cron/cost-insights-hourly/route.test.ts @@ -6,7 +6,7 @@ const mockSentryLog = jest.fn(); jest.mock('@/lib/utils.server', () => ({ sentryLogger: jest.fn(() => mockSentryLog) })); import { runCostInsightHourlySweep } from '@/lib/cost-insights/jobs'; -import { GET } from './route'; +import { GET, maxDuration } from './route'; const mockRunCostInsightHourlySweep = jest.mocked(runCostInsightHourlySweep); @@ -26,6 +26,10 @@ function summary(failedOwners: Array<{ owner: { type: 'user'; id: string }; erro terminalized: 0, failed: 0, }, + alreadyRunning: false, + deadlineReached: false, + ownerCycleComplete: true, + cycleId: 'cycle-1', }; } @@ -70,4 +74,8 @@ describe('GET /api/cron/cost-insights-hourly', () => { }); expect(mockSentryLog).not.toHaveBeenCalled(); }); + + test('exports a bounded function duration for resumable sweeps', () => { + expect(maxDuration).toBe(300); + }); }); diff --git a/apps/web/src/app/api/cron/cost-insights-hourly/route.ts b/apps/web/src/app/api/cron/cost-insights-hourly/route.ts index 002d685006..19ad87693c 100644 --- a/apps/web/src/app/api/cron/cost-insights-hourly/route.ts +++ b/apps/web/src/app/api/cron/cost-insights-hourly/route.ts @@ -9,6 +9,8 @@ if (!CRON_SECRET) { throw new Error('CRON_SECRET is not configured in environment variables'); } +export const maxDuration = 300; + export async function GET(request: Request) { const authHeader = request.headers.get('authorization'); const expectedAuth = `Bearer ${CRON_SECRET}`; @@ -40,6 +42,9 @@ export async function GET(request: Request) { { success: !hasFailures, partialFailure: hasFailures, + complete: summary.ownerCycleComplete, + deadlineReached: summary.deadlineReached, + alreadyRunning: summary.alreadyRunning, summary, timestamp: new Date().toISOString(), }, diff --git a/apps/web/src/lib/cost-insights/canonical-sources.test.ts b/apps/web/src/lib/cost-insights/canonical-sources.test.ts index b82e4bcaad..dc7770f1d9 100644 --- a/apps/web/src/lib/cost-insights/canonical-sources.test.ts +++ b/apps/web/src/lib/cost-insights/canonical-sources.test.ts @@ -1,5 +1,6 @@ import { aggregateCanonicalCostInsightDrivers, + aggregateNormalizedCanonicalCostInsightDrivers, loadCanonicalCostInsightAggregationsByHour, mapAiGatewayCanonicalDriver, mapCodingPlanCanonicalDriver, @@ -187,6 +188,26 @@ describe('Cost Insights canonical source mapping', () => { ]); }); + test('rejects matching driver digests with different canonical dimensions', async () => { + const input = mapExaCanonicalDriver({ + owner: userOwner, + actorUserId: 'user-1', + path: '/answer', + totalMicrodollars: 4, + spendRecordCount: 1, + }).driver; + const normalized = await aggregateCanonicalCostInsightDrivers([input]); + const driver = normalized.drivers[0]; + if (!driver) throw new Error('Expected normalized canonical driver.'); + + expect(() => + aggregateNormalizedCanonicalCostInsightDrivers([ + driver, + { ...driver, providerKey: 'different-provider' }, + ]) + ).toThrow('Canonical Cost Insights driver digest collision.'); + }); + test('keeps multi-hour canonical source aggregates separated by UTC hour', async () => { const execute = jest .fn() diff --git a/apps/web/src/lib/cost-insights/canonical-sources.ts b/apps/web/src/lib/cost-insights/canonical-sources.ts index 391d8052e7..37719d1ebf 100644 --- a/apps/web/src/lib/cost-insights/canonical-sources.ts +++ b/apps/web/src/lib/cost-insights/canonical-sources.ts @@ -416,7 +416,21 @@ export function mapKiloClawCanonicalDriver(params: { }; } -function aggregateNormalizedCanonicalCostInsightDrivers( +function driverDimensionsMatch( + left: CanonicalCostInsightDriverAggregate, + right: CanonicalCostInsightDriverAggregate +): boolean { + return ( + left.source === right.source && + left.productKey === right.productKey && + left.featureKey === right.featureKey && + left.modelOrPlanKey === right.modelOrPlanKey && + left.providerKey === right.providerKey && + left.actorUserId === right.actorUserId + ); +} + +export function aggregateNormalizedCanonicalCostInsightDrivers( inputs: CanonicalCostInsightDriverAggregate[] ): { totals: CanonicalCostInsightOwnerTotal[]; @@ -434,6 +448,12 @@ function aggregateNormalizedCanonicalCostInsightDrivers( } const totalKey = `${ownerIdentity(input.owner)}:${input.category}`; + const driverIdentity = `${totalKey}:${input.driverKey}`; + const priorDriver = drivers.get(driverIdentity); + if (priorDriver && !driverDimensionsMatch(priorDriver, input)) { + throw new Error('Canonical Cost Insights driver digest collision.'); + } + const priorTotal = totals.get(totalKey); if (priorTotal) { priorTotal.totalMicrodollars = addSafeInteger( @@ -455,8 +475,6 @@ function aggregateNormalizedCanonicalCostInsightDrivers( }); } - const driverIdentity = `${totalKey}:${input.driverKey}`; - const priorDriver = drivers.get(driverIdentity); if (priorDriver) { priorDriver.totalMicrodollars = addSafeInteger( priorDriver.totalMicrodollars, diff --git a/apps/web/src/lib/cost-insights/evaluation.ts b/apps/web/src/lib/cost-insights/evaluation.ts index ba394577f9..b63e89f2df 100644 --- a/apps/web/src/lib/cost-insights/evaluation.ts +++ b/apps/web/src/lib/cost-insights/evaluation.ts @@ -579,6 +579,7 @@ type CostInsightClaimedDirtyOwner = { owned_by_user_id: string | null; owned_by_organization_id: string | null; generation: string | number | bigint; + claim_token: string; }; export type CostInsightDirtyEvaluationSummary = { @@ -599,6 +600,7 @@ async function claimDirtyCostInsightOwners( database: CostInsightRootDatabase, options: { limit: number; owner?: CostInsightSpendOwner } ): Promise { + const claimToken = crypto.randomUUID(); const ownerPredicate = options.owner ? options.owner.type === 'user' ? sql`dirty_owner.owned_by_user_id = ${options.owner.id} AND dirty_owner.owned_by_organization_id IS NULL` @@ -623,6 +625,7 @@ async function claimDirtyCostInsightOwners( UPDATE cost_insight_evaluation_dirty_owners dirty_owner SET claimed_at = CURRENT_TIMESTAMP, + claim_token = ${claimToken}, attempt_count = dirty_owner.attempt_count + 1, last_error_redacted = NULL, updated_at = CURRENT_TIMESTAMP @@ -632,7 +635,8 @@ async function claimDirtyCostInsightOwners( dirty_owner.id, dirty_owner.owned_by_user_id, dirty_owner.owned_by_organization_id, - dirty_owner.generation + dirty_owner.generation, + dirty_owner.claim_token `); return result.rows; } @@ -646,16 +650,19 @@ async function completeDirtyCostInsightOwner( DELETE FROM cost_insight_evaluation_dirty_owners WHERE id = ${row.id} AND generation = ${row.generation} + AND claim_token = ${row.claim_token} RETURNING id ) UPDATE cost_insight_evaluation_dirty_owners dirty_owner SET claimed_at = NULL, + claim_token = NULL, attempt_count = 0, next_attempt_at = CURRENT_TIMESTAMP, last_error_redacted = NULL, updated_at = CURRENT_TIMESTAMP WHERE dirty_owner.id = ${row.id} + AND dirty_owner.claim_token = ${row.claim_token} AND NOT EXISTS (SELECT 1 FROM removed) `); } @@ -669,10 +676,12 @@ async function failDirtyCostInsightOwner( UPDATE cost_insight_evaluation_dirty_owners SET claimed_at = NULL, + claim_token = NULL, next_attempt_at = CURRENT_TIMESTAMP + INTERVAL '5 minutes', last_error_redacted = ${error.slice(0, 500)}, updated_at = CURRENT_TIMESTAMP WHERE id = ${row.id} + AND claim_token = ${row.claim_token} `); } @@ -683,9 +692,11 @@ export async function processPendingCostInsightEvaluations( owner?: CostInsightSpendOwner; asOf?: string; recoverCompletedHour?: boolean; + concurrency?: number; } = {} ): Promise { const limit = options.limit ?? 25; + const concurrency = Math.max(1, Math.min(options.concurrency ?? 4, limit)); const summary: CostInsightDirtyEvaluationSummary = { claimed: 0, evaluatedOwners: [], @@ -694,33 +705,35 @@ export async function processPendingCostInsightEvaluations( while (summary.claimed < limit) { const rows = await claimDirtyCostInsightOwners(database, { - limit: limit - summary.claimed, + limit: Math.min(concurrency, limit - summary.claimed), owner: options.owner, }); if (rows.length === 0) break; summary.claimed += rows.length; - for (const row of rows) { - const owner = ownerFromDirtyRow(row); - try { - await evaluateCostInsightsForOwner(database, owner, { - asOf: options.asOf, - recoverCompletedHour: options.recoverCompletedHour, - }); - await completeDirtyCostInsightOwner(database, row); - if ( - !summary.evaluatedOwners.some( - evaluatedOwner => evaluatedOwner.type === owner.type && evaluatedOwner.id === owner.id - ) - ) { - summary.evaluatedOwners.push(owner); + await Promise.all( + rows.map(async row => { + const owner = ownerFromDirtyRow(row); + try { + await evaluateCostInsightsForOwner(database, owner, { + asOf: options.asOf, + recoverCompletedHour: options.recoverCompletedHour, + }); + await completeDirtyCostInsightOwner(database, row); + if ( + !summary.evaluatedOwners.some( + evaluatedOwner => evaluatedOwner.type === owner.type && evaluatedOwner.id === owner.id + ) + ) { + summary.evaluatedOwners.push(owner); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await failDirtyCostInsightOwner(database, row, message); + summary.failedOwners.push({ owner, error: message }); } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - await failDirtyCostInsightOwner(database, row, message); - summary.failedOwners.push({ owner, error: message }); - } - } + }) + ); } return summary; diff --git a/apps/web/src/lib/cost-insights/hourly-sweep-repository.ts b/apps/web/src/lib/cost-insights/hourly-sweep-repository.ts new file mode 100644 index 0000000000..76e9aaab53 --- /dev/null +++ b/apps/web/src/lib/cost-insights/hourly-sweep-repository.ts @@ -0,0 +1,133 @@ +import { sql } from 'drizzle-orm'; + +import type { CostInsightOwnerCursor, CostInsightRootDatabase } from './repository'; + +const OWNER_EVALUATION_JOB_NAME = 'owner_evaluation'; + +type SweepCheckpointRow = { + cycle_id: string; + cycle_as_of: string | Date; + cohort_created_before: string | Date; + cursor_owner_type: 'user' | 'organization' | null; + cursor_owner_id: string | null; + lease_token: string; +}; + +export type CostInsightHourlySweepLease = { + leaseToken: string; + cycleId: string; + cycleAsOf: string; + cohortCreatedBefore: string; + cursor: CostInsightOwnerCursor | null; +}; + +function normalizeTimestamp(value: string | Date): string { + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); +} + +function cursorFromRow(row: SweepCheckpointRow): CostInsightOwnerCursor | null { + if (!row.cursor_owner_type || !row.cursor_owner_id) return null; + return { ownerType: row.cursor_owner_type, ownerId: row.cursor_owner_id }; +} + +export async function acquireCostInsightHourlySweepLease( + database: CostInsightRootDatabase, + options: { asOf: string; leaseSeconds: number } +): Promise { + const leaseToken = crypto.randomUUID(); + const cycleId = crypto.randomUUID(); + + return await database.transaction(async tx => { + await tx.execute(sql` + INSERT INTO cost_insight_hourly_sweep_checkpoints (job_name) + VALUES (${OWNER_EVALUATION_JOB_NAME}) + ON CONFLICT (job_name) DO NOTHING + `); + const result = await tx.execute(sql` + UPDATE cost_insight_hourly_sweep_checkpoints + SET + cycle_id = COALESCE(cycle_id, ${cycleId}::uuid), + cycle_as_of = COALESCE(cycle_as_of, ${options.asOf}::timestamptz), + cohort_created_before = COALESCE(cohort_created_before, CURRENT_TIMESTAMP), + lease_token = ${leaseToken}::uuid, + lease_expires_at = CURRENT_TIMESTAMP + make_interval(secs => ${options.leaseSeconds}), + started_at = COALESCE(started_at, CURRENT_TIMESTAMP), + updated_at = CURRENT_TIMESTAMP + WHERE job_name = ${OWNER_EVALUATION_JOB_NAME} + AND (lease_token IS NULL OR lease_expires_at <= CURRENT_TIMESTAMP) + RETURNING + cycle_id::text, + cycle_as_of, + cohort_created_before, + cursor_owner_type, + cursor_owner_id, + lease_token::text + `); + const row = result.rows[0]; + if (!row) return null; + return { + leaseToken: row.lease_token, + cycleId: row.cycle_id, + cycleAsOf: normalizeTimestamp(row.cycle_as_of), + cohortCreatedBefore: normalizeTimestamp(row.cohort_created_before), + cursor: cursorFromRow(row), + }; + }); +} + +export async function advanceCostInsightHourlySweepCursor( + database: CostInsightRootDatabase, + leaseToken: string, + cursor: CostInsightOwnerCursor +): Promise { + const result = await database.execute<{ job_name: string }>(sql` + UPDATE cost_insight_hourly_sweep_checkpoints + SET + cursor_owner_type = ${cursor.ownerType}, + cursor_owner_id = ${cursor.ownerId}, + updated_at = CURRENT_TIMESTAMP + WHERE job_name = ${OWNER_EVALUATION_JOB_NAME} + AND lease_token = ${leaseToken}::uuid + RETURNING job_name + `); + return result.rows.length > 0; +} + +export async function completeCostInsightHourlySweepCycle( + database: CostInsightRootDatabase, + leaseToken: string +): Promise { + const result = await database.execute<{ job_name: string }>(sql` + UPDATE cost_insight_hourly_sweep_checkpoints + SET + cycle_id = NULL, + cycle_as_of = NULL, + cohort_created_before = NULL, + cursor_owner_type = NULL, + cursor_owner_id = NULL, + lease_token = NULL, + lease_expires_at = NULL, + started_at = NULL, + last_completed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE job_name = ${OWNER_EVALUATION_JOB_NAME} + AND lease_token = ${leaseToken}::uuid + RETURNING job_name + `); + return result.rows.length > 0; +} + +export async function releaseCostInsightHourlySweepLease( + database: CostInsightRootDatabase, + leaseToken: string +): Promise { + await database.execute(sql` + UPDATE cost_insight_hourly_sweep_checkpoints + SET + lease_token = NULL, + lease_expires_at = NULL, + updated_at = CURRENT_TIMESTAMP + WHERE job_name = ${OWNER_EVALUATION_JOB_NAME} + AND lease_token = ${leaseToken}::uuid + `); +} diff --git a/apps/web/src/lib/cost-insights/jobs.ts b/apps/web/src/lib/cost-insights/jobs.ts index ecd7455e76..372be4fea6 100644 --- a/apps/web/src/lib/cost-insights/jobs.ts +++ b/apps/web/src/lib/cost-insights/jobs.ts @@ -1,19 +1,38 @@ import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; +import { sql } from 'drizzle-orm'; +import pLimit from 'p-limit'; +import { evaluateCostInsightsForOwner, processPendingCostInsightEvaluations } from './evaluation'; +import { + acquireCostInsightHourlySweepLease, + advanceCostInsightHourlySweepCursor, + completeCostInsightHourlySweepCycle, + releaseCostInsightHourlySweepLease, +} from './hourly-sweep-repository'; import { dispatchPendingCostInsightNotifications } from './notifications'; import { deleteExpiredCostInsightEvents, - listEnabledCostInsightOwners, + listEnabledCostInsightOwnerPage, type CostInsightDatabase, type CostInsightRootDatabase, } from './repository'; -import { evaluateCostInsightsForOwner, processPendingCostInsightEvaluations } from './evaluation'; + +const DEFAULT_SWEEP_TIME_BUDGET_MS = 240_000; +const OWNER_PAGE_SIZE = 20; +const OWNER_CONCURRENCY = 4; +const DIRTY_OWNER_LIMIT = 20; +const NOTIFICATION_LIMIT = 25; +const CHECKPOINT_LEASE_SECONDS = 5 * 60; export type CostInsightHourlySweepSummary = { evaluatedOwners: number; failedOwners: Array<{ owner: CostInsightSpendOwner; error: string }>; dirtyEvaluations: Awaited>; notifications: Awaited>; + alreadyRunning: boolean; + deadlineReached: boolean; + ownerCycleComplete: boolean; + cycleId: string | null; }; function ownerKey(owner: CostInsightSpendOwner): string { @@ -22,13 +41,23 @@ function ownerKey(owner: CostInsightSpendOwner): string { export async function runCostInsightHourlySweep( database: CostInsightRootDatabase, - options: { asOf?: string; dirtyOwnerLimit?: number } = {} + options: { + asOf?: string; + dirtyOwnerLimit?: number; + timeBudgetMs?: number; + ownerPageSize?: number; + ownerConcurrency?: number; + notificationLimit?: number; + } = {} ): Promise { const asOf = options.asOf ?? new Date().toISOString(); + const deadline = performance.now() + (options.timeBudgetMs ?? DEFAULT_SWEEP_TIME_BUDGET_MS); + const ownerConcurrency = options.ownerConcurrency ?? OWNER_CONCURRENCY; const dirtyEvaluations = await processPendingCostInsightEvaluations(database, { - limit: options.dirtyOwnerLimit ?? 25, + limit: options.dirtyOwnerLimit ?? DIRTY_OWNER_LIMIT, asOf, recoverCompletedHour: true, + concurrency: ownerConcurrency, }); const claimedOwnerKeys = new Set( [ @@ -36,26 +65,78 @@ export async function runCostInsightHourlySweep( ...dirtyEvaluations.failedOwners.map(row => row.owner), ].map(ownerKey) ); - const owners = (await listEnabledCostInsightOwners(database)).filter( - owner => !claimedOwnerKeys.has(ownerKey(owner)) - ); const failedOwners: CostInsightHourlySweepSummary['failedOwners'] = [ ...dirtyEvaluations.failedOwners, ]; let evaluatedOwners = dirtyEvaluations.evaluatedOwners.length; + let alreadyRunning = false; + let deadlineReached = false; + let ownerCycleComplete = false; + let cycleId: string | null = null; - for (const owner of owners) { + const lease = await acquireCostInsightHourlySweepLease(database, { + asOf, + leaseSeconds: CHECKPOINT_LEASE_SECONDS, + }); + if (!lease) { + alreadyRunning = true; + } else { + cycleId = lease.cycleId; + const limitOwnerWork = pLimit(ownerConcurrency); + let cursor = lease.cursor; try { - await evaluateCostInsightsForOwner(database, owner, { - asOf, - recoverCompletedHour: true, - }); - evaluatedOwners += 1; - } catch (error) { - failedOwners.push({ - owner, - error: error instanceof Error ? error.message : String(error), - }); + while (performance.now() < deadline) { + const page = await listEnabledCostInsightOwnerPage(database, { + cohortCreatedBefore: lease.cohortCreatedBefore, + after: cursor, + limit: options.ownerPageSize ?? OWNER_PAGE_SIZE, + }); + if (page.owners.length === 0) { + ownerCycleComplete = true; + await completeCostInsightHourlySweepCycle(database, lease.leaseToken); + break; + } + const outcomes = await Promise.all( + page.owners.map(owner => + limitOwnerWork(async () => { + if (claimedOwnerKeys.has(ownerKey(owner))) return { owner, skipped: true as const }; + try { + await evaluateCostInsightsForOwner(database, owner, { + asOf: lease.cycleAsOf, + recoverCompletedHour: true, + }); + return { owner, skipped: false as const, error: null }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await markCostInsightOwnerDirtyForRetry(database, owner, message); + return { owner, skipped: false as const, error: message }; + } + }) + ) + ); + for (const outcome of outcomes) { + if (outcome.skipped) continue; + if (outcome.error) { + failedOwners.push({ owner: outcome.owner, error: outcome.error }); + } else { + evaluatedOwners += 1; + } + } + if (page.nextCursor) { + await advanceCostInsightHourlySweepCursor(database, lease.leaseToken, page.nextCursor); + cursor = page.nextCursor; + } + if (!page.hasMore) { + ownerCycleComplete = true; + await completeCostInsightHourlySweepCycle(database, lease.leaseToken); + break; + } + } + deadlineReached = !ownerCycleComplete && performance.now() >= deadline; + } finally { + if (!ownerCycleComplete) { + await releaseCostInsightHourlySweepLease(database, lease.leaseToken); + } } } @@ -63,10 +144,56 @@ export async function runCostInsightHourlySweep( evaluatedOwners, failedOwners, dirtyEvaluations, - notifications: await dispatchPendingCostInsightNotifications(database), + notifications: await dispatchPendingCostInsightNotifications( + database, + options.notificationLimit ?? NOTIFICATION_LIMIT + ), + alreadyRunning, + deadlineReached, + ownerCycleComplete, + cycleId, }; } +async function markCostInsightOwnerDirtyForRetry( + database: CostInsightRootDatabase, + owner: CostInsightSpendOwner, + error: string +): Promise { + const ownerColumns = + owner.type === 'user' + ? { ownedByUserId: owner.id, ownedByOrganizationId: null } + : { ownedByUserId: null, ownedByOrganizationId: owner.id }; + const conflictTarget = + owner.type === 'user' + ? sql.raw('(owned_by_user_id) WHERE owned_by_organization_id IS NULL') + : sql.raw('(owned_by_organization_id) WHERE owned_by_user_id IS NULL'); + await database.execute(sql` + INSERT INTO cost_insight_evaluation_dirty_owners AS dirty_owner ( + owned_by_user_id, + owned_by_organization_id, + dirty_at, + next_attempt_at, + last_error_redacted + ) VALUES ( + ${ownerColumns.ownedByUserId}, + ${ownerColumns.ownedByOrganizationId}, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + INTERVAL '5 minutes', + ${error.slice(0, 500)} + ) + ON CONFLICT ${conflictTarget} + DO UPDATE SET + generation = dirty_owner.generation + 1, + dirty_at = CURRENT_TIMESTAMP, + next_attempt_at = CURRENT_TIMESTAMP + INTERVAL '5 minutes', + claimed_at = NULL, + claim_token = NULL, + last_error_redacted = ${error.slice(0, 500)}, + updated_at = CURRENT_TIMESTAMP + `); +} + export async function runCostInsightEventRetentionCleanup( database: CostInsightDatabase ): Promise<{ deletedEvents: number; cutoff: string }> { diff --git a/apps/web/src/lib/cost-insights/notifications.integration.test.ts b/apps/web/src/lib/cost-insights/notifications.integration.test.ts index 9344a97642..9a89ab0014 100644 --- a/apps/web/src/lib/cost-insights/notifications.integration.test.ts +++ b/apps/web/src/lib/cost-insights/notifications.integration.test.ts @@ -4,14 +4,17 @@ import { cost_insight_notification_deliveries, kilocode_users, } from '@kilocode/db/schema'; -import { eq } from 'drizzle-orm'; +import { eq, sql } from 'drizzle-orm'; import { db } from '@/lib/drizzle'; -import { claimPendingCostInsightNotificationDeliveries } from './notifications'; +import { + claimPendingCostInsightNotificationDeliveries, + dispatchPendingCostInsightNotifications, +} from './notifications'; const testUserIds = new Set(); -async function createDelivery(): Promise<{ deliveryId: string; userId: string }> { +async function createDelivery(): Promise<{ deliveryId: string; eventId: string; userId: string }> { const userId = `cost-insights-notification-${crypto.randomUUID()}`; testUserIds.add(userId); await db.insert(kilocode_users).values({ @@ -38,7 +41,7 @@ async function createDelivery(): Promise<{ deliveryId: string; userId: string }> .values({ event_id: event.id, recipient_user_id: userId }) .returning({ id: cost_insight_notification_deliveries.id }); if (!delivery) throw new Error('Test delivery insert returned no row.'); - return { deliveryId: delivery.id, userId }; + return { deliveryId: delivery.id, eventId: event.id, userId }; } afterEach(async () => { @@ -93,4 +96,29 @@ describe('Cost Insights notification claims', () => { last_error_redacted: 'stale_claim_attempts_exhausted', }); }); + + test('skips malformed event snapshots without retrying delivery', async () => { + const { deliveryId, eventId } = await createDelivery(); + await db.execute(sql` + UPDATE ${cost_insight_events} + SET snapshot = '"malformed"'::jsonb + WHERE id = ${eventId} + `); + + await expect(dispatchPendingCostInsightNotifications(db, 1)).resolves.toMatchObject({ + claimed: 1, + sent: 0, + skipped: 1, + failed: 0, + }); + const [delivery] = await db + .select() + .from(cost_insight_notification_deliveries) + .where(eq(cost_insight_notification_deliveries.id, deliveryId)); + + expect(delivery).toMatchObject({ + status: 'skipped', + last_error_redacted: 'invalid_event_snapshot', + }); + }); }); diff --git a/apps/web/src/lib/cost-insights/notifications.ts b/apps/web/src/lib/cost-insights/notifications.ts index fd7fc5449c..0a2b22215f 100644 --- a/apps/web/src/lib/cost-insights/notifications.ts +++ b/apps/web/src/lib/cost-insights/notifications.ts @@ -1,5 +1,8 @@ import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; -import { cost_insight_notification_deliveries } from '@kilocode/db/schema'; +import { + cost_insight_notification_deliveries, + type CostInsightEventSnapshot, +} from '@kilocode/db/schema'; import type { CostInsightAlertKind } from '@kilocode/db/schema-types'; import { eq, sql } from 'drizzle-orm'; @@ -8,6 +11,7 @@ import { sendCostInsightSpendAlertEmail } from '@/lib/email'; import { getCostInsightOwnerName, hasCurrentCostInsightAccess, + parsePersistedCostInsightEventSnapshot, type CostInsightDatabase, } from './repository'; import { costInsightOwnerBasePath } from './owner'; @@ -26,14 +30,11 @@ export type CostInsightClaimedDeliveryRow = { description: string; alert_kind: CostInsightAlertKind | null; attempt_count: number; - snapshot: { - thresholdMicrodollars?: number | null; - rolling24HourMicrodollars?: number | null; - rolling7DayMicrodollars?: number | null; - rolling30DayMicrodollars?: number | null; - currentHourVariableMicrodollars?: number | null; - anomalyThresholdMicrodollars?: number | null; - }; + snapshot: unknown; +}; + +type ParsedCostInsightClaimedDeliveryRow = Omit & { + snapshot: CostInsightEventSnapshot; }; export type CostInsightNotificationDispatchSummary = { @@ -61,7 +62,7 @@ function money(value: number | null | undefined): string { }).format(microdollarsToUsd(value)); } -function amountLabels(row: CostInsightClaimedDeliveryRow): { +function amountLabels(row: ParsedCostInsightClaimedDeliveryRow): { primaryAmountLabel: string; secondaryAmountLabel: string; } { @@ -239,35 +240,46 @@ export async function dispatchPendingCostInsightNotifications( }; for (const row of rows) { - const owner = ownerFromDelivery(row); - const hasAccess = await hasCurrentCostInsightAccess(database, owner, row.recipient_user_id); + const snapshot = parsePersistedCostInsightEventSnapshot(row.snapshot); + if (!snapshot) { + await markDeliverySkipped(database, row.delivery_id, 'invalid_event_snapshot'); + summary.skipped += 1; + continue; + } + const parsedRow = { ...row, snapshot }; + const owner = ownerFromDelivery(parsedRow); + const hasAccess = await hasCurrentCostInsightAccess( + database, + owner, + parsedRow.recipient_user_id + ); if (!hasAccess) { - await markDeliverySkipped(database, row.delivery_id, 'recipient_not_authorized'); + await markDeliverySkipped(database, parsedRow.delivery_id, 'recipient_not_authorized'); summary.skipped += 1; continue; } try { - const labels = amountLabels(row); - const result = await sendCostInsightSpendAlertEmail(row.recipient_email, { + const labels = amountLabels(parsedRow); + const result = await sendCostInsightSpendAlertEmail(parsedRow.recipient_email, { ownerLabel: await getCostInsightOwnerName(database, owner), - alertTitle: row.title, - alertDescription: row.description, + alertTitle: parsedRow.title, + alertDescription: parsedRow.description, primaryAmountLabel: labels.primaryAmountLabel, secondaryAmountLabel: labels.secondaryAmountLabel, reviewUrl: `${NEXTAUTH_URL}${costInsightOwnerBasePath(owner)}`, }); if (!result.sent) { - await markDeliveryFailed(database, row.delivery_id, result.reason); + await markDeliveryFailed(database, parsedRow.delivery_id, result.reason); summary.failed += 1; continue; } - await markDeliverySent(database, row.delivery_id); + await markDeliverySent(database, parsedRow.delivery_id); summary.sent += 1; } catch (error) { await markDeliveryFailed( database, - row.delivery_id, + parsedRow.delivery_id, error instanceof Error ? error.message : String(error) ); summary.failed += 1; diff --git a/apps/web/src/lib/cost-insights/repository.ts b/apps/web/src/lib/cost-insights/repository.ts index 5805cd77ff..9cb6e62361 100644 --- a/apps/web/src/lib/cost-insights/repository.ts +++ b/apps/web/src/lib/cost-insights/repository.ts @@ -5,6 +5,7 @@ import { cost_insight_notification_deliveries, cost_insight_owner_configs, cost_insight_owner_states, + CostInsightEventSnapshotSchema, kilocode_users, organization_memberships, organizations, @@ -30,6 +31,7 @@ import { export type CostInsightDatabase = typeof db | DrizzleTransaction; export type CostInsightRootDatabase = typeof db; export type CostInsightEventFilter = 'all' | 'alerts' | 'suggestions' | 'reviews' | 'settings'; +export type CostInsightOwnerCursor = { ownerType: CostInsightSpendOwner['type']; ownerId: string }; const eventTypesByFilter = { alerts: ['anomaly_alert', 'threshold_crossed'], @@ -69,6 +71,17 @@ export type CostInsightEventInput = { dedupeKey?: string | null; }; +export function parsePersistedCostInsightEventSnapshot( + value: unknown +): CostInsightEventSnapshot | null { + const result = CostInsightEventSnapshotSchema.safeParse(value); + return result.success ? result.data : null; +} + +function validateCostInsightEventSnapshot(value: unknown): CostInsightEventSnapshot { + return CostInsightEventSnapshotSchema.parse(value); +} + type CostInsightEventWriter = ( database: CostInsightDatabase, input: CostInsightEventInput @@ -380,6 +393,7 @@ export async function createCostInsightEvent( database: CostInsightDatabase, input: CostInsightEventInput ): Promise<{ id: string; created: boolean }> { + const snapshot = validateCostInsightEventSnapshot(input.snapshot ?? {}); const [event] = await database .insert(cost_insight_events) .values({ @@ -391,7 +405,7 @@ export async function createCostInsightEvent( actor_user_id: input.actorUserId ?? null, title: input.title, description: input.description, - snapshot: input.snapshot ?? {}, + snapshot, dedupe_key: input.dedupeKey ?? null, }) .onConflictDoNothing() @@ -483,13 +497,68 @@ export async function listEnabledCostInsightOwners( }); } +type EnabledCostInsightOwnerRow = { + owner_type: CostInsightSpendOwner['type']; + owner_id: string; +}; + +export async function listEnabledCostInsightOwnerPage( + database: CostInsightDatabase, + options: { + cohortCreatedBefore: string; + after?: CostInsightOwnerCursor | null; + limit: number; + } +): Promise<{ + owners: CostInsightSpendOwner[]; + nextCursor: CostInsightOwnerCursor | null; + hasMore: boolean; +}> { + if (!Number.isSafeInteger(options.limit) || options.limit <= 0) { + throw new Error('Cost Insights owner page limit must be a positive safe integer.'); + } + const afterPredicate = options.after + ? sql`(owner_type, owner_id) > (${options.after.ownerType}, ${options.after.ownerId})` + : sql`TRUE`; + const result = await database.execute(sql` + WITH active_owners AS ( + SELECT 'organization'::text AS owner_type, owned_by_organization_id::text AS owner_id + FROM cost_insight_owner_configs + WHERE owned_by_organization_id IS NOT NULL + AND created_at < ${options.cohortCreatedBefore} + AND (spend_alerts_enabled = TRUE OR cost_suggestions_enabled = TRUE) + UNION ALL + SELECT 'user'::text AS owner_type, owned_by_user_id AS owner_id + FROM cost_insight_owner_configs + WHERE owned_by_user_id IS NOT NULL + AND created_at < ${options.cohortCreatedBefore} + AND (spend_alerts_enabled = TRUE OR cost_suggestions_enabled = TRUE) + ) + SELECT owner_type, owner_id + FROM active_owners + WHERE ${afterPredicate} + ORDER BY owner_type ASC, owner_id ASC + LIMIT ${options.limit + 1} + `); + const pageRows = result.rows.slice(0, options.limit); + const owners = pageRows.map(row => ({ type: row.owner_type, id: row.owner_id })); + const last = pageRows.at(-1); + return { + owners, + nextCursor: last + ? { ownerType: last.owner_type, ownerId: last.owner_id } + : (options.after ?? null), + hasMore: result.rows.length > options.limit, + }; +} + export async function listCostInsightEvents( database: CostInsightDatabase, owner: CostInsightSpendOwner, options: { limit?: number; offset?: number; filter?: CostInsightEventFilter } = {} ) { const eventTypes = eventTypesForFilter(options.filter ?? 'all'); - return await database + const rows = await database .select({ id: cost_insight_events.id, eventType: cost_insight_events.event_type, @@ -513,6 +582,10 @@ export async function listCostInsightEvents( .orderBy(desc(cost_insight_events.occurred_at), desc(cost_insight_events.id)) .limit(options.limit ?? 50) .offset(options.offset ?? 0); + return rows.map(row => ({ + ...row, + snapshot: parsePersistedCostInsightEventSnapshot(row.snapshot) ?? {}, + })); } export async function countCostInsightEvents( @@ -592,15 +665,23 @@ export async function getCostInsightDashboardState( .from(cost_insight_events) .where(inArray(cost_insight_events.id, eventIds)); - const eventsById = new Map(events.map(event => [event.id, event])); + const eventsById = new Map( + events.map(event => [ + event.id, + { ...event, snapshot: parsePersistedCostInsightEventSnapshot(event.snapshot) ?? {} }, + ]) + ); const activeAnomalyEpisodeId = state?.activeAnomalyEpisodeId ?? state?.activeAnomalyEventId; - if (activeAnomalyEpisodeId && state?.activeAnomalySnapshot) { - eventsById.set(activeAnomalyEpisodeId, { - id: activeAnomalyEpisodeId, - event_type: 'anomaly_alert', - alert_kind: 'anomaly', - snapshot: state.activeAnomalySnapshot, - }); + if (activeAnomalyEpisodeId) { + const parsedSnapshot = parsePersistedCostInsightEventSnapshot(state?.activeAnomalySnapshot); + if (parsedSnapshot || !eventsById.has(activeAnomalyEpisodeId)) { + eventsById.set(activeAnomalyEpisodeId, { + id: activeAnomalyEpisodeId, + event_type: 'anomaly_alert', + alert_kind: 'anomaly', + snapshot: parsedSnapshot ?? {}, + }); + } } const thresholdSnapshots = [ { @@ -624,13 +705,16 @@ export async function getCostInsightDashboardState( ]; for (const threshold of thresholdSnapshots) { const episodeId = threshold.id ?? threshold.fallbackId; - if (!episodeId || !threshold.snapshot) continue; - eventsById.set(episodeId, { - id: episodeId, - event_type: 'threshold_crossed', - alert_kind: threshold.alertKind, - snapshot: threshold.snapshot, - }); + if (!episodeId) continue; + const parsedSnapshot = parsePersistedCostInsightEventSnapshot(threshold.snapshot); + if (parsedSnapshot || !eventsById.has(episodeId)) { + eventsById.set(episodeId, { + id: episodeId, + event_type: 'threshold_crossed', + alert_kind: threshold.alertKind, + snapshot: parsedSnapshot ?? {}, + }); + } } return { state: state ?? null, events: [...eventsById.values()] }; @@ -645,6 +729,7 @@ export async function markCostInsightAnomalyEpisode( snapshot: CostInsightEventSnapshot; } ): Promise { + const snapshot = validateCostInsightEventSnapshot(params.snapshot); const state = await getOrCreateCostInsightOwnerState(database, params.owner); await database .update(cost_insight_owner_states) @@ -652,7 +737,7 @@ export async function markCostInsightAnomalyEpisode( active_anomaly_event_id: params.eventId, active_anomaly_episode_id: params.eventId, active_anomaly_hour_start: params.hourStart, - active_anomaly_snapshot: params.snapshot, + active_anomaly_snapshot: snapshot, active_anomaly_reviewed_at: null, updated_at: sql`now()`, }) @@ -669,6 +754,7 @@ export async function markCostInsightThresholdEpisode( snapshot: CostInsightEventSnapshot; } ): Promise { + const snapshot = validateCostInsightEventSnapshot(params.snapshot); const state = await getOrCreateCostInsightOwnerState(database, params.owner); const values = (() => { if (params.alertKind === 'threshold_7d') { @@ -677,7 +763,7 @@ export async function markCostInsightThresholdEpisode( active_rolling_7_day_threshold_event_id: params.eventId, active_rolling_7_day_threshold_episode_id: params.eventId, rolling_7_day_threshold_crossing_started_at: params.crossedAt, - active_rolling_7_day_threshold_snapshot: params.snapshot, + active_rolling_7_day_threshold_snapshot: snapshot, rolling_7_day_threshold_reviewed_at: null, rolling_7_day_threshold_recovered_at: null, updated_at: sql`now()`, @@ -689,7 +775,7 @@ export async function markCostInsightThresholdEpisode( active_rolling_30_day_threshold_event_id: params.eventId, active_rolling_30_day_threshold_episode_id: params.eventId, rolling_30_day_threshold_crossing_started_at: params.crossedAt, - active_rolling_30_day_threshold_snapshot: params.snapshot, + active_rolling_30_day_threshold_snapshot: snapshot, rolling_30_day_threshold_reviewed_at: null, rolling_30_day_threshold_recovered_at: null, updated_at: sql`now()`, @@ -700,7 +786,7 @@ export async function markCostInsightThresholdEpisode( active_threshold_event_id: params.eventId, active_threshold_episode_id: params.eventId, threshold_crossing_started_at: params.crossedAt, - active_threshold_snapshot: params.snapshot, + active_threshold_snapshot: snapshot, threshold_reviewed_at: null, threshold_recovered_at: null, updated_at: sql`now()`, diff --git a/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts b/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts index f139a0a9cf..12e00a1b7b 100644 --- a/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts +++ b/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from '@jest/globals'; +import { buildCostInsightDriver } from '@kilocode/db/cost-insights-rollups'; import { eq } from 'drizzle-orm'; import { db } from '@/lib/drizzle'; @@ -41,6 +42,17 @@ async function initializeCoverage(): Promise { }); } +async function aiGatewayDriver(modelOrPlanKey: string, actorUserId: string) { + return await buildCostInsightDriver({ + source: 'ai_gateway', + productKey: 'other', + featureKey: 'other', + modelOrPlanKey, + providerKey: 'provider', + actorUserId, + }); +} + afterEach(async () => { await db .delete(cost_insight_rollup_degraded_intervals) @@ -247,6 +259,268 @@ describe('Cost Insights spend repository integration', () => { }); }); + test('merges complete rollup interior with exact canonical boundaries before ranking', async () => { + const userId = await createUser(); + await initializeCoverage(); + const boundaryDrivers = [ + { model: 'model-1', cost: 60 }, + { model: 'model-2', cost: 60 }, + { model: 'model-3', cost: 40 }, + { model: 'model-4', cost: 30 }, + { model: 'model-5', cost: 20 }, + { model: 'model-6', cost: 10 }, + ]; + const model6Driver = await aiGatewayDriver('model-6', userId); + const scheduledDriver = await buildCostInsightDriver({ + source: 'coding_plan', + productKey: 'coding-plan', + featureKey: 'renewal', + modelOrPlanKey: 'plan-1', + providerKey: 'provider-1', + actorUserId: userId, + }); + await db.insert(cost_insight_owner_hour_driver_buckets).values([ + { + owned_by_user_id: userId, + hour_start: '2026-06-01T13:00:00.000Z', + spend_category: 'variable', + driver_key: model6Driver.driverKey, + source: 'ai_gateway', + product_key: 'other', + feature_key: 'other', + model_or_plan_key: 'model-6', + provider_key: 'provider', + actor_user_id: userId, + total_microdollars: 100, + spend_record_count: 2, + }, + { + owned_by_user_id: userId, + hour_start: '2026-06-01T14:00:00.000Z', + spend_category: 'scheduled', + driver_key: scheduledDriver.driverKey, + source: 'coding_plan', + product_key: 'coding-plan', + feature_key: 'renewal', + model_or_plan_key: 'plan-1', + provider_key: 'provider-1', + actor_user_id: userId, + total_microdollars: 7, + spend_record_count: 1, + }, + ]); + await db.insert(microdollar_usage).values([ + ...boundaryDrivers.map(({ model, cost }, index) => ({ + id: crypto.randomUUID(), + kilo_user_id: userId, + cost, + input_tokens: 0, + output_tokens: 0, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: `2026-06-01T12:45:0${index}.000Z`, + provider: 'provider', + model, + requested_model: model, + inference_provider: 'provider', + has_error: false, + })), + { + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 5, + input_tokens: 0, + output_tokens: 0, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-02T12:15:00.000Z', + provider: 'provider', + model: 'model-5', + requested_model: 'model-5', + inference_provider: 'provider', + has_error: false, + }, + ]); + + await expect( + getOwnerRolling24HourDriverEvidenceExact(db, { + owner: { type: 'user', id: userId }, + asOf: '2026-06-02T12:30:00.000Z', + }) + ).resolves.toEqual({ + asOf: '2026-06-02T12:30:00.000Z', + windowStart: '2026-06-01T12:30:00.000Z', + variableMicrodollars: 325, + scheduledMicrodollars: 7, + totalMicrodollars: 332, + topDrivers: [ + expect.objectContaining({ + modelOrPlanKey: 'model-6', + totalMicrodollars: 110, + spendRecordCount: 3, + }), + expect.objectContaining({ modelOrPlanKey: 'model-1', totalMicrodollars: 60 }), + expect.objectContaining({ modelOrPlanKey: 'model-2', totalMicrodollars: 60 }), + expect.objectContaining({ modelOrPlanKey: 'model-3', totalMicrodollars: 40 }), + expect.objectContaining({ modelOrPlanKey: 'model-4', totalMicrodollars: 30 }), + ], + }); + }); + + test('uses full canonical exact evidence when interior rollup coverage is incomplete', async () => { + const userId = await createUser(); + await initializeCoverage(); + const driver = await aiGatewayDriver('fallback-model', userId); + await db.insert(cost_insight_owner_hour_driver_buckets).values({ + owned_by_user_id: userId, + hour_start: '2026-06-01T13:00:00.000Z', + spend_category: 'variable', + driver_key: driver.driverKey, + source: 'ai_gateway', + product_key: 'other', + feature_key: 'other', + model_or_plan_key: 'fallback-model', + provider_key: 'provider', + actor_user_id: userId, + total_microdollars: 999, + spend_record_count: 1, + }); + await db.insert(cost_insight_rollup_degraded_intervals).values({ + start_hour: '2026-06-01T13:00:00.000Z', + end_hour_exclusive: '2026-06-01T14:00:00.000Z', + reason: 'capture_bypass', + }); + await db.insert(microdollar_usage).values( + [ + { createdAt: '2026-06-01T12:45:00.000Z', cost: 3 }, + { createdAt: '2026-06-01T13:15:00.000Z', cost: 11 }, + { createdAt: '2026-06-02T12:15:00.000Z', cost: 4 }, + ].map(({ createdAt, cost }) => ({ + id: crypto.randomUUID(), + kilo_user_id: userId, + cost, + input_tokens: 0, + output_tokens: 0, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: createdAt, + provider: 'provider', + model: 'fallback-model', + requested_model: 'fallback-model', + inference_provider: 'provider', + has_error: false, + })) + ); + + await expect( + getOwnerRolling24HourDriverEvidenceExact(db, { + owner: { type: 'user', id: userId }, + asOf: '2026-06-02T12:30:00.000Z', + }) + ).resolves.toEqual({ + asOf: '2026-06-02T12:30:00.000Z', + windowStart: '2026-06-01T12:30:00.000Z', + variableMicrodollars: 18, + scheduledMicrodollars: 0, + totalMicrodollars: 18, + topDrivers: [ + expect.objectContaining({ + modelOrPlanKey: 'fallback-model', + totalMicrodollars: 18, + spendRecordCount: 3, + }), + ], + }); + }); + + test('rejects matching category and driver keys with mismatched dimensions', async () => { + const userId = await createUser(); + await initializeCoverage(); + const driver = await aiGatewayDriver('collision-model', userId); + await db.insert(cost_insight_owner_hour_driver_buckets).values({ + owned_by_user_id: userId, + hour_start: '2026-06-01T13:00:00.000Z', + spend_category: 'variable', + driver_key: driver.driverKey, + source: 'ai_gateway', + product_key: 'other', + feature_key: 'other', + model_or_plan_key: 'collision-model', + provider_key: 'mismatched-provider', + actor_user_id: userId, + total_microdollars: 10, + spend_record_count: 1, + }); + await db.insert(microdollar_usage).values({ + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 1, + input_tokens: 0, + output_tokens: 0, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-01T12:45:00.000Z', + provider: 'provider', + model: 'collision-model', + requested_model: 'collision-model', + inference_provider: 'provider', + has_error: false, + abuse_classification: 0, + }); + + await expect( + getOwnerRolling24HourDriverEvidenceExact(db, { + owner: { type: 'user', id: userId }, + asOf: '2026-06-02T12:30:00.000Z', + }) + ).rejects.toThrow('mismatched dimensions across exact evidence fragments'); + }); + + test('rejects unsafe sums while merging exact driver fragments', async () => { + const userId = await createUser(); + await initializeCoverage(); + const driver = await aiGatewayDriver('overflow-model', userId); + await db.insert(cost_insight_owner_hour_driver_buckets).values({ + owned_by_user_id: userId, + hour_start: '2026-06-01T13:00:00.000Z', + spend_category: 'variable', + driver_key: driver.driverKey, + source: 'ai_gateway', + product_key: 'other', + feature_key: 'other', + model_or_plan_key: 'overflow-model', + provider_key: 'provider', + actor_user_id: userId, + total_microdollars: Number.MAX_SAFE_INTEGER, + spend_record_count: 1, + }); + await db.insert(microdollar_usage).values({ + id: crypto.randomUUID(), + kilo_user_id: userId, + cost: 1, + input_tokens: 0, + output_tokens: 0, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-01T12:45:00.000Z', + provider: 'provider', + model: 'overflow-model', + requested_model: 'overflow-model', + inference_provider: 'provider', + has_error: false, + abuse_classification: 0, + }); + + await expect( + getOwnerRolling24HourDriverEvidenceExact(db, { + owner: { type: 'user', id: userId }, + asOf: '2026-06-02T12:30:00.000Z', + }) + ).rejects.toThrow( + 'exact driver total_microdollars is outside the JavaScript safe-integer range' + ); + }); + test('filters top drivers to the requested hour and spend category', async () => { const userId = await createUser(); const baseDriver = { diff --git a/apps/web/src/lib/cost-insights/spend-repository.ts b/apps/web/src/lib/cost-insights/spend-repository.ts index 528230e38f..6bd48e9f4a 100644 --- a/apps/web/src/lib/cost-insights/spend-repository.ts +++ b/apps/web/src/lib/cost-insights/spend-repository.ts @@ -155,6 +155,14 @@ type InteriorTotalRow = { total_microdollars: string | number | bigint; }; +type InteriorDriverRow = TopDriverRow & { + driver_key: string; +}; + +type MergeableSpendDriver = OwnerTopSpendDriver & { + driverKey: string; +}; + type DatabaseTimestampRow = { value: string | Date; }; @@ -621,7 +629,7 @@ async function getInteriorRollupTotals( function compareTopSpendDrivers(left: OwnerTopSpendDriver, right: OwnerTopSpendDriver): number { if (left.totalMicrodollars !== right.totalMicrodollars) { - return right.totalMicrodollars - left.totalMicrodollars; + return left.totalMicrodollars > right.totalMicrodollars ? -1 : 1; } const leftKey = [ left.category, @@ -644,6 +652,197 @@ function compareTopSpendDrivers(left: OwnerTopSpendDriver, right: OwnerTopSpendD return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; } +function compareMergeableSpendDrivers( + left: MergeableSpendDriver, + right: MergeableSpendDriver +): number { + const dimensionOrder = compareTopSpendDrivers(left, right); + if (dimensionOrder !== 0) return dimensionOrder; + return left.driverKey < right.driverKey ? -1 : left.driverKey > right.driverKey ? 1 : 0; +} + +function haveMatchingDriverDimensions( + left: MergeableSpendDriver, + right: MergeableSpendDriver +): boolean { + return ( + left.source === right.source && + left.productKey === right.productKey && + left.featureKey === right.featureKey && + left.modelOrPlanKey === right.modelOrPlanKey && + left.providerKey === right.providerKey && + left.actorUserId === right.actorUserId + ); +} + +function mergeSpendDrivers(driverGroups: MergeableSpendDriver[][]): MergeableSpendDriver[] { + const merged = new Map(); + for (const drivers of driverGroups) { + for (const driver of drivers) { + const identity = JSON.stringify([driver.category, driver.driverKey]); + const existing = merged.get(identity); + if (!existing) { + merged.set(identity, { + ...driver, + totalMicrodollars: sumSafe( + 0, + driver.totalMicrodollars, + 'exact driver total_microdollars' + ), + spendRecordCount: sumSafe(0, driver.spendRecordCount, 'exact driver spend_record_count'), + }); + continue; + } + if (!haveMatchingDriverDimensions(existing, driver)) { + throw new Error( + `Cost Insights driver ${identity} has mismatched dimensions across exact evidence fragments.` + ); + } + existing.totalMicrodollars = sumSafe( + existing.totalMicrodollars, + driver.totalMicrodollars, + 'exact driver total_microdollars' + ); + existing.spendRecordCount = sumSafe( + existing.spendRecordCount, + driver.spendRecordCount, + 'exact driver spend_record_count' + ); + } + } + return [...merged.values()]; +} + +function summarizeSpendDrivers(drivers: MergeableSpendDriver[]): { + variableMicrodollars: number; + scheduledMicrodollars: number; + totalMicrodollars: number; + topDrivers: OwnerTopSpendDriver[]; +} { + let variableMicrodollars = 0; + let scheduledMicrodollars = 0; + for (const driver of drivers) { + if (driver.category === 'variable') { + variableMicrodollars = sumSafe( + variableMicrodollars, + driver.totalMicrodollars, + 'exact driver variable total' + ); + } else { + scheduledMicrodollars = sumSafe( + scheduledMicrodollars, + driver.totalMicrodollars, + 'exact driver scheduled total' + ); + } + } + const topDrivers = [...drivers] + .sort(compareMergeableSpendDrivers) + .slice(0, COST_INSIGHT_MAX_TOP_DRIVERS) + .map(driver => ({ + category: driver.category, + source: driver.source, + productKey: driver.productKey, + featureKey: driver.featureKey, + modelOrPlanKey: driver.modelOrPlanKey, + providerKey: driver.providerKey, + actorUserId: driver.actorUserId, + totalMicrodollars: driver.totalMicrodollars, + spendRecordCount: driver.spendRecordCount, + })); + return { + variableMicrodollars, + scheduledMicrodollars, + totalMicrodollars: sumSafe( + variableMicrodollars, + scheduledMicrodollars, + 'exact driver total microdollars' + ), + topDrivers, + }; +} + +async function getInteriorRollupDrivers( + executor: CostInsightQueryExecutor, + owner: CostInsightSpendOwner, + startInclusive: string, + endExclusive: string +): Promise { + if (startInclusive === endExclusive) return []; + const result = await executor.execute(sql` + SELECT + ${cost_insight_owner_hour_driver_buckets.spend_category} AS spend_category, + ${cost_insight_owner_hour_driver_buckets.driver_key} AS driver_key, + ${cost_insight_owner_hour_driver_buckets.source} AS source, + ${cost_insight_owner_hour_driver_buckets.product_key} AS product_key, + ${cost_insight_owner_hour_driver_buckets.feature_key} AS feature_key, + ${cost_insight_owner_hour_driver_buckets.model_or_plan_key} AS model_or_plan_key, + ${cost_insight_owner_hour_driver_buckets.provider_key} AS provider_key, + ${cost_insight_owner_hour_driver_buckets.actor_user_id} AS actor_user_id, + SUM(${cost_insight_owner_hour_driver_buckets.total_microdollars})::text + AS total_microdollars, + SUM(${cost_insight_owner_hour_driver_buckets.spend_record_count})::text + AS spend_record_count + FROM ${cost_insight_owner_hour_driver_buckets} + WHERE ${cost_insight_owner_hour_driver_buckets.hour_start} >= ${startInclusive} + AND ${cost_insight_owner_hour_driver_buckets.hour_start} < ${endExclusive} + AND ${ownerPredicate( + owner, + sql`${cost_insight_owner_hour_driver_buckets.owned_by_user_id}`, + sql`${cost_insight_owner_hour_driver_buckets.owned_by_organization_id}` + )} + GROUP BY 1, 2, 3, 4, 5, 6, 7, 8 + ORDER BY 1, 2, 3, 4, 5, 6, 7, 8 + `); + return result.rows.map(row => ({ + category: row.spend_category, + driverKey: row.driver_key, + source: row.source, + productKey: row.product_key, + featureKey: row.feature_key, + modelOrPlanKey: row.model_or_plan_key, + providerKey: row.provider_key, + actorUserId: row.actor_user_id, + totalMicrodollars: parseSafeDatabaseInteger( + row.total_microdollars, + 'interior driver total_microdollars' + ), + spendRecordCount: parseSafeDatabaseInteger( + row.spend_record_count, + 'interior driver spend_record_count' + ), + })); +} + +async function getCanonicalDrivers( + executor: CostInsightQueryExecutor, + params: { + owner: CostInsightSpendOwner; + startInclusive: string; + endExclusive: string; + } +): Promise { + if (params.startInclusive === params.endExclusive) return []; + const aggregation = await loadCanonicalCostInsightAggregation(executor, params); + return aggregation.drivers.map(driver => { + if (driver.owner.type !== params.owner.type || driver.owner.id !== params.owner.id) { + throw new Error('Canonical Cost Insights driver resolved to the wrong Spend owner.'); + } + return { + category: driver.category, + driverKey: driver.driverKey, + source: driver.source, + productKey: driver.productKey, + featureKey: driver.featureKey, + modelOrPlanKey: driver.modelOrPlanKey, + providerKey: driver.providerKey, + actorUserId: driver.actorUserId, + totalMicrodollars: driver.totalMicrodollars, + spendRecordCount: driver.spendRecordCount, + }; + }); +} + export async function getOwnerSpendDriverEvidenceExact( primaryDatabase: ExactRollingDatabase, params: { @@ -728,21 +927,64 @@ export async function getOwnerRollingDriverEvidenceExact( ): Promise { const requestedAsOf = params.asOf === undefined ? undefined : requireUtcTimestamp(params.asOf, 'asOf'); - const asOf = requestedAsOf ?? new Date().toISOString(); - const windowStart = getRollingWindowFragments(asOf, params.windowHours).windowStart; - const evidence = await getOwnerSpendDriverEvidenceExact(primaryDatabase, { - owner: params.owner, - startInclusive: windowStart, - endExclusive: asOf, - }); - return { - asOf, - windowStart, - variableMicrodollars: evidence.variableMicrodollars, - scheduledMicrodollars: evidence.scheduledMicrodollars, - totalMicrodollars: evidence.totalMicrodollars, - topDrivers: evidence.topDrivers, - }; + + return primaryDatabase.transaction( + async transaction => { + const asOfResult = await transaction.execute(sql` + SELECT COALESCE(${requestedAsOf ?? null}::timestamptz, CURRENT_TIMESTAMP) AS value + `); + const asOfRow = asOfResult.rows[0]; + if (!asOfRow) { + throw new Error('Cost Insights exact driver query could not establish an as-of value.'); + } + const fragments = getRollingWindowFragments( + normalizeDatabaseTimestamp(asOfRow.value, 'as_of'), + params.windowHours + ); + const coverage = + fragments.interiorStart === fragments.interiorEnd + ? null + : await getCostInsightRollupCoverage(transaction, { + startHour: fragments.interiorStart, + endHourExclusive: fragments.interiorEnd, + }); + + const mergedDrivers = + coverage?.isFullyCovered === false + ? mergeSpendDrivers([ + await getCanonicalDrivers(transaction, { + owner: params.owner, + startInclusive: fragments.windowStart, + endExclusive: fragments.asOf, + }), + ]) + : mergeSpendDrivers([ + await getInteriorRollupDrivers( + transaction, + params.owner, + fragments.interiorStart, + fragments.interiorEnd + ), + await getCanonicalDrivers(transaction, { + owner: params.owner, + startInclusive: fragments.windowStart, + endExclusive: fragments.oldestBoundaryEnd, + }), + await getCanonicalDrivers(transaction, { + owner: params.owner, + startInclusive: fragments.currentBoundaryStart, + endExclusive: fragments.asOf, + }), + ]); + const evidence = summarizeSpendDrivers(mergedDrivers); + return { + asOf: fragments.asOf, + windowStart: fragments.windowStart, + ...evidence, + }; + }, + { isolationLevel: 'repeatable read', accessMode: 'read only' } + ); } export async function getOwnerRolling24HourDriverEvidenceExact( diff --git a/packages/db/src/cost-insight-event-snapshot.test.ts b/packages/db/src/cost-insight-event-snapshot.test.ts new file mode 100644 index 0000000000..14adf149a4 --- /dev/null +++ b/packages/db/src/cost-insight-event-snapshot.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, test } from '@jest/globals'; + +import { CostInsightEventSnapshotSchema } from './schema'; + +const validDriver = { + spendCategory: 'variable', + source: 'ai_gateway', + productKey: 'direct-gateway', + featureKey: 'chat_completions', + modelOrPlanKey: 'anthropic/claude-sonnet-4', + providerKey: 'anthropic', + actorUserId: 'user-1', + totalMicrodollars: 125, + spendRecordCount: 1, +}; + +describe('CostInsightEventSnapshotSchema', () => { + test('accepts empty and current alert snapshots', () => { + expect(CostInsightEventSnapshotSchema.parse({})).toEqual({}); + expect( + CostInsightEventSnapshotSchema.parse({ + thresholdMicrodollars: 1_000_000, + thresholdWindow: 'rolling_7d', + rolling7DayMicrodollars: 1_500_000, + topDrivers: [validDriver], + topDriversWindow: { + startInclusive: '2026-06-01 00:00:00+00', + endExclusive: '2026-06-08T00:00:00.000Z', + spendCategory: 'variable', + }, + }) + ).toMatchObject({ rolling7DayMicrodollars: 1_500_000 }); + }); + + test('accepts config and suggestion snapshots', () => { + expect( + CostInsightEventSnapshotSchema.parse({ + changedFields: { spendAlertsEnabled: { old: false, new: true } }, + settings: { + spendAlertsEnabled: true, + anomalyAlertsEnabled: true, + costSuggestionsEnabled: true, + spendThresholdMicrodollars: null, + spend7DayThresholdMicrodollars: 1, + spend30DayThresholdMicrodollars: 2, + }, + suggestion: { + suggestionKey: 'a'.repeat(64), + evidenceWindowStart: '2026-06-01T00:00:00.000Z', + evidenceWindowEnd: '2026-06-02T00:00:00.000Z', + observedMicrodollars: 1, + ctaHref: '/pricing', + }, + }) + ).toMatchObject({ settings: { spendAlertsEnabled: true } }); + }); + + test('strips unknown fields while preserving known fields', () => { + expect( + CostInsightEventSnapshotSchema.parse({ + rolling24HourMicrodollars: 50, + unknown: 'ignored', + topDrivers: [{ ...validDriver, unknown: 'ignored' }], + }) + ).toEqual({ rolling24HourMicrodollars: 50, topDrivers: [validDriver] }); + }); + + test.each([null, [], 'snapshot'])('rejects non-object top-level value %#', value => { + expect(() => CostInsightEventSnapshotSchema.parse(value)).toThrow(); + }); + + test('rejects invalid enums, unsafe amounts, too many drivers, and reversed windows', () => { + expect(() => + CostInsightEventSnapshotSchema.parse({ topDrivers: [{ ...validDriver, source: 'exa' }] }) + ).toThrow(); + expect(() => + CostInsightEventSnapshotSchema.parse({ + topDrivers: [{ ...validDriver, totalMicrodollars: -1 }], + }) + ).toThrow(); + expect(() => + CostInsightEventSnapshotSchema.parse({ + topDrivers: Array.from({ length: 6 }, () => validDriver), + }) + ).toThrow(); + expect(() => + CostInsightEventSnapshotSchema.parse({ + topDriversWindow: { + startInclusive: '2026-06-02T00:00:00.000Z', + endExclusive: '2026-06-01T00:00:00.000Z', + }, + }) + ).toThrow(); + }); +}); diff --git a/packages/db/src/cost-insights-rollups.test.ts b/packages/db/src/cost-insights-rollups.test.ts index 985ded7788..bdc3cb9e10 100644 --- a/packages/db/src/cost-insights-rollups.test.ts +++ b/packages/db/src/cost-insights-rollups.test.ts @@ -80,6 +80,32 @@ async function withCostInsightFixture( } } +function costInsightOwnerHourLockKey( + owner: CaptureCostInsightSpendInput['owner'], + hourStart: string +): string { + return [ + 'cost-insight-owner-hour:v1', + `${owner.type.length}:${owner.type}`, + `${owner.id.length}:${owner.id}`, + `${hourStart.length}:${hourStart}`, + ].join('|'); +} + +function createDeferred(): { promise: Promise; resolve: () => void } { + let resolvePromise: (() => void) | undefined; + const promise = new Promise(resolve => { + resolvePromise = resolve; + }); + return { + promise, + resolve: () => { + if (!resolvePromise) throw new Error('Deferred promise is not initialized.'); + resolvePromise(); + }, + }; +} + function captureInput( fixture: CostInsightTestFixture, overrides: Partial = {} @@ -210,6 +236,42 @@ describe('Cost Insights rollup capture', () => { expect(execute).toHaveBeenCalledTimes(1); }); + it('allows live capture while another transaction holds the shared owner-hour lock', async () => { + await withCostInsightFixture(async fixture => { + const input = captureInput(fixture); + const hourStart = getCostInsightUtcHourStart(input.occurredAt); + const lockKey = costInsightOwnerHourLockKey(input.owner, hourStart); + const lockAcquired = createDeferred(); + const releaseLock = createDeferred(); + const lockHolder = testDatabase.db.transaction(async tx => { + await tx.execute( + sql`SELECT pg_catalog.pg_advisory_xact_lock_shared( + pg_catalog.hashtextextended(${lockKey}, 0::bigint) + )` + ); + lockAcquired.resolve(); + await releaseLock.promise; + }); + + await lockAcquired.promise; + try { + await testDatabase.db.transaction(async tx => { + await tx.execute(sql`SET LOCAL lock_timeout = '500ms'`); + await captureCostInsightSpend(tx, input); + }); + } finally { + releaseLock.resolve(); + await lockHolder; + } + + const [total] = await testDatabase.db + .select() + .from(cost_insight_owner_hour_totals) + .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); + expect(total).toMatchObject({ total_microdollars: 125, spend_record_count: 1 }); + }); + }); + it('adds totals before matching driver buckets under a non-UTC database timezone', async () => { await withCostInsightFixture(async fixture => { await testDatabase.db.transaction(async tx => { diff --git a/packages/db/src/cost-insights-rollups.ts b/packages/db/src/cost-insights-rollups.ts index d5e57b767a..9f63d2166e 100644 --- a/packages/db/src/cost-insights-rollups.ts +++ b/packages/db/src/cost-insights-rollups.ts @@ -327,7 +327,7 @@ async function writeCostInsightSpend( ${values.spendRecordCount}::bigint AS spend_record_count, ${lockKey}::text AS lock_key ), owner_hour_lock AS MATERIALIZED ( - SELECT pg_catalog.pg_advisory_xact_lock( + SELECT pg_catalog.pg_advisory_xact_lock_shared( pg_catalog.hashtextextended(capture_input.lock_key, 0::bigint) ) AS acquired FROM capture_input diff --git a/packages/db/src/migrations/0174_bizarre_piledriver.sql b/packages/db/src/migrations/0174_minor_the_call.sql similarity index 90% rename from packages/db/src/migrations/0174_bizarre_piledriver.sql rename to packages/db/src/migrations/0174_minor_the_call.sql index 610b52ac68..5de3dc9c6a 100644 --- a/packages/db/src/migrations/0174_bizarre_piledriver.sql +++ b/packages/db/src/migrations/0174_minor_the_call.sql @@ -34,13 +34,15 @@ CREATE TABLE "cost_insight_evaluation_dirty_owners" ( "dirty_at" timestamp with time zone DEFAULT now() NOT NULL, "next_attempt_at" timestamp with time zone DEFAULT now() NOT NULL, "claimed_at" timestamp with time zone, + "claim_token" uuid, "attempt_count" integer DEFAULT 0 NOT NULL, "last_error_redacted" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "cost_insight_evaluation_dirty_owners_owner_check" CHECK (("cost_insight_evaluation_dirty_owners"."owned_by_user_id" IS NOT NULL AND "cost_insight_evaluation_dirty_owners"."owned_by_organization_id" IS NULL) OR ("cost_insight_evaluation_dirty_owners"."owned_by_user_id" IS NULL AND "cost_insight_evaluation_dirty_owners"."owned_by_organization_id" IS NOT NULL)), CONSTRAINT "cost_insight_evaluation_dirty_owners_generation_check" CHECK ("cost_insight_evaluation_dirty_owners"."generation" > 0 AND "cost_insight_evaluation_dirty_owners"."generation" <= 9007199254740991), - CONSTRAINT "cost_insight_evaluation_dirty_owners_attempt_count_check" CHECK ("cost_insight_evaluation_dirty_owners"."attempt_count" >= 0) + CONSTRAINT "cost_insight_evaluation_dirty_owners_attempt_count_check" CHECK ("cost_insight_evaluation_dirty_owners"."attempt_count" >= 0), + CONSTRAINT "cost_insight_evaluation_dirty_owners_claim_token_check" CHECK (("cost_insight_evaluation_dirty_owners"."claimed_at" IS NULL AND "cost_insight_evaluation_dirty_owners"."claim_token" IS NULL) OR ("cost_insight_evaluation_dirty_owners"."claimed_at" IS NOT NULL AND "cost_insight_evaluation_dirty_owners"."claim_token" IS NOT NULL)) ); --> statement-breakpoint CREATE TABLE "cost_insight_events" ( @@ -66,6 +68,26 @@ CREATE TABLE "cost_insight_events" ( CONSTRAINT "cost_insight_events_suggestion_kind_presence_check" CHECK (("cost_insight_events"."event_type" IN ('suggestion_created', 'suggestion_dismissed') AND "cost_insight_events"."suggestion_kind" IS NOT NULL) OR ("cost_insight_events"."event_type" NOT IN ('suggestion_created', 'suggestion_dismissed') AND "cost_insight_events"."suggestion_kind" IS NULL)) ); --> statement-breakpoint +CREATE TABLE "cost_insight_hourly_sweep_checkpoints" ( + "job_name" text PRIMARY KEY NOT NULL, + "cycle_id" uuid, + "cycle_as_of" timestamp with time zone, + "cohort_created_before" timestamp with time zone, + "cursor_owner_type" text, + "cursor_owner_id" text, + "lease_token" uuid, + "lease_expires_at" timestamp with time zone, + "started_at" timestamp with time zone, + "last_completed_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "cost_insight_hourly_sweep_job_name_check" CHECK ("cost_insight_hourly_sweep_checkpoints"."job_name" <> ''), + CONSTRAINT "cost_insight_hourly_sweep_cursor_owner_type_check" CHECK ("cost_insight_hourly_sweep_checkpoints"."cursor_owner_type" IS NULL OR "cost_insight_hourly_sweep_checkpoints"."cursor_owner_type" IN ('user', 'organization')), + CONSTRAINT "cost_insight_hourly_sweep_cursor_check" CHECK (("cost_insight_hourly_sweep_checkpoints"."cursor_owner_type" IS NULL AND "cost_insight_hourly_sweep_checkpoints"."cursor_owner_id" IS NULL) OR ("cost_insight_hourly_sweep_checkpoints"."cursor_owner_type" IS NOT NULL AND "cost_insight_hourly_sweep_checkpoints"."cursor_owner_id" IS NOT NULL)), + CONSTRAINT "cost_insight_hourly_sweep_lease_check" CHECK (("cost_insight_hourly_sweep_checkpoints"."lease_token" IS NULL AND "cost_insight_hourly_sweep_checkpoints"."lease_expires_at" IS NULL) OR ("cost_insight_hourly_sweep_checkpoints"."lease_token" IS NOT NULL AND "cost_insight_hourly_sweep_checkpoints"."lease_expires_at" IS NOT NULL)), + CONSTRAINT "cost_insight_hourly_sweep_cycle_check" CHECK (("cost_insight_hourly_sweep_checkpoints"."cycle_id" IS NULL AND "cost_insight_hourly_sweep_checkpoints"."cycle_as_of" IS NULL AND "cost_insight_hourly_sweep_checkpoints"."cohort_created_before" IS NULL AND "cost_insight_hourly_sweep_checkpoints"."started_at" IS NULL) OR ("cost_insight_hourly_sweep_checkpoints"."cycle_id" IS NOT NULL AND "cost_insight_hourly_sweep_checkpoints"."cycle_as_of" IS NOT NULL AND "cost_insight_hourly_sweep_checkpoints"."cohort_created_before" IS NOT NULL AND "cost_insight_hourly_sweep_checkpoints"."started_at" IS NOT NULL)) +); +--> statement-breakpoint CREATE TABLE "cost_insight_notification_deliveries" ( "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, "event_id" uuid NOT NULL, @@ -274,6 +296,8 @@ CREATE INDEX "IDX_cost_insight_notification_deliveries_event" ON "cost_insight_n CREATE UNIQUE INDEX "UQ_cost_insight_owner_configs_user" ON "cost_insight_owner_configs" USING btree ("owned_by_user_id") WHERE "cost_insight_owner_configs"."owned_by_organization_id" is null;--> statement-breakpoint CREATE UNIQUE INDEX "UQ_cost_insight_owner_configs_org" ON "cost_insight_owner_configs" USING btree ("owned_by_organization_id") WHERE "cost_insight_owner_configs"."owned_by_user_id" is null;--> statement-breakpoint CREATE INDEX "IDX_cost_insight_owner_configs_evaluation" ON "cost_insight_owner_configs" USING btree ("updated_at","id") WHERE "cost_insight_owner_configs"."spend_alerts_enabled" = TRUE OR "cost_insight_owner_configs"."cost_suggestions_enabled" = TRUE;--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_owner_configs_user_active" ON "cost_insight_owner_configs" USING btree ("owned_by_user_id") WHERE "cost_insight_owner_configs"."owned_by_user_id" IS NOT NULL AND ("cost_insight_owner_configs"."spend_alerts_enabled" = TRUE OR "cost_insight_owner_configs"."cost_suggestions_enabled" = TRUE);--> statement-breakpoint +CREATE INDEX "IDX_cost_insight_owner_configs_org_active" ON "cost_insight_owner_configs" USING btree ("owned_by_organization_id") WHERE "cost_insight_owner_configs"."owned_by_organization_id" IS NOT NULL AND ("cost_insight_owner_configs"."spend_alerts_enabled" = TRUE OR "cost_insight_owner_configs"."cost_suggestions_enabled" = TRUE);--> statement-breakpoint CREATE UNIQUE INDEX "UQ_cost_insight_driver_buckets_user" ON "cost_insight_owner_hour_driver_buckets" USING btree ("owned_by_user_id","hour_start","spend_category","driver_key") WHERE "cost_insight_owner_hour_driver_buckets"."owned_by_organization_id" is null;--> statement-breakpoint CREATE UNIQUE INDEX "UQ_cost_insight_driver_buckets_org" ON "cost_insight_owner_hour_driver_buckets" USING btree ("owned_by_organization_id","hour_start","spend_category","driver_key") WHERE "cost_insight_owner_hour_driver_buckets"."owned_by_user_id" is null;--> statement-breakpoint CREATE INDEX "IDX_cost_insight_driver_buckets_hour" ON "cost_insight_owner_hour_driver_buckets" USING btree ("hour_start");--> statement-breakpoint @@ -284,4 +308,6 @@ CREATE UNIQUE INDEX "UQ_cost_insight_owner_states_user" ON "cost_insight_owner_s CREATE UNIQUE INDEX "UQ_cost_insight_owner_states_org" ON "cost_insight_owner_states" USING btree ("owned_by_organization_id") WHERE "cost_insight_owner_states"."owned_by_user_id" is null;--> statement-breakpoint CREATE INDEX "IDX_cost_insight_owner_states_unreviewed_user" ON "cost_insight_owner_states" USING btree ("owned_by_user_id","updated_at") WHERE "cost_insight_owner_states"."owned_by_user_id" IS NOT NULL AND (("cost_insight_owner_states"."active_anomaly_episode_id" IS NOT NULL AND "cost_insight_owner_states"."active_anomaly_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL));--> statement-breakpoint CREATE INDEX "IDX_cost_insight_owner_states_unreviewed_org" ON "cost_insight_owner_states" USING btree ("owned_by_organization_id","updated_at") WHERE "cost_insight_owner_states"."owned_by_organization_id" IS NOT NULL AND (("cost_insight_owner_states"."active_anomaly_episode_id" IS NOT NULL AND "cost_insight_owner_states"."active_anomaly_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_7_day_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_7_day_threshold_reviewed_at" IS NULL) OR ("cost_insight_owner_states"."active_rolling_30_day_threshold_episode_id" IS NOT NULL AND "cost_insight_owner_states"."rolling_30_day_threshold_reviewed_at" IS NULL));--> statement-breakpoint -CREATE INDEX "IDX_cost_insight_degraded_intervals_unresolved" ON "cost_insight_rollup_degraded_intervals" USING btree ("start_hour","end_hour_exclusive") WHERE "cost_insight_rollup_degraded_intervals"."resolved_at" is null; \ No newline at end of file +CREATE INDEX "IDX_cost_insight_degraded_intervals_unresolved" ON "cost_insight_rollup_degraded_intervals" USING btree ("start_hour","end_hour_exclusive") WHERE "cost_insight_rollup_degraded_intervals"."resolved_at" is null;--> statement-breakpoint +CREATE INDEX "IDX_coding_plan_terms_credit_transaction" ON "coding_plan_terms" USING btree ("credit_transaction_id");--> statement-breakpoint +CREATE INDEX "idx_microdollar_usage_org_created_at" ON "microdollar_usage" USING btree ("organization_id","created_at") WHERE "microdollar_usage"."organization_id" is not null; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0174_snapshot.json b/packages/db/src/migrations/meta/0174_snapshot.json index 64e0c55666..d8d0fac401 100644 --- a/packages/db/src/migrations/meta/0174_snapshot.json +++ b/packages/db/src/migrations/meta/0174_snapshot.json @@ -1,5 +1,5 @@ { - "id": "f1321a3d-546a-452a-bcd8-b1361304f467", + "id": "0848ab22-ba80-4ffe-a478-0253a92a6a6d", "prevId": "06265fd6-428e-4b7d-9092-dba307d6255e", "version": "7", "dialect": "postgresql", @@ -7728,6 +7728,21 @@ "concurrently": false, "method": "btree", "with": {} + }, + "IDX_coding_plan_terms_credit_transaction": { + "name": "IDX_coding_plan_terms_credit_transaction", + "columns": [ + { + "expression": "credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": { @@ -8566,6 +8581,12 @@ "primaryKey": false, "notNull": false }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "attempt_count": { "name": "attempt_count", "type": "integer", @@ -8704,6 +8725,10 @@ "cost_insight_evaluation_dirty_owners_attempt_count_check": { "name": "cost_insight_evaluation_dirty_owners_attempt_count_check", "value": "\"cost_insight_evaluation_dirty_owners\".\"attempt_count\" >= 0" + }, + "cost_insight_evaluation_dirty_owners_claim_token_check": { + "name": "cost_insight_evaluation_dirty_owners_claim_token_check", + "value": "(\"cost_insight_evaluation_dirty_owners\".\"claimed_at\" IS NULL AND \"cost_insight_evaluation_dirty_owners\".\"claim_token\" IS NULL) OR (\"cost_insight_evaluation_dirty_owners\".\"claimed_at\" IS NOT NULL AND \"cost_insight_evaluation_dirty_owners\".\"claim_token\" IS NOT NULL)" } }, "isRLSEnabled": false @@ -9001,6 +9026,114 @@ }, "isRLSEnabled": false }, + "public.cost_insight_hourly_sweep_checkpoints": { + "name": "cost_insight_hourly_sweep_checkpoints", + "schema": "", + "columns": { + "job_name": { + "name": "job_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cycle_as_of": { + "name": "cycle_as_of", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cohort_created_before": { + "name": "cohort_created_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cursor_owner_type": { + "name": "cursor_owner_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_owner_id": { + "name": "cursor_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_completed_at": { + "name": "last_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_hourly_sweep_job_name_check": { + "name": "cost_insight_hourly_sweep_job_name_check", + "value": "\"cost_insight_hourly_sweep_checkpoints\".\"job_name\" <> ''" + }, + "cost_insight_hourly_sweep_cursor_owner_type_check": { + "name": "cost_insight_hourly_sweep_cursor_owner_type_check", + "value": "\"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_type\" IS NULL OR \"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_type\" IN ('user', 'organization')" + }, + "cost_insight_hourly_sweep_cursor_check": { + "name": "cost_insight_hourly_sweep_cursor_check", + "value": "(\"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_type\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_id\" IS NULL) OR (\"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_type\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_id\" IS NOT NULL)" + }, + "cost_insight_hourly_sweep_lease_check": { + "name": "cost_insight_hourly_sweep_lease_check", + "value": "(\"cost_insight_hourly_sweep_checkpoints\".\"lease_token\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"lease_expires_at\" IS NULL) OR (\"cost_insight_hourly_sweep_checkpoints\".\"lease_token\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"lease_expires_at\" IS NOT NULL)" + }, + "cost_insight_hourly_sweep_cycle_check": { + "name": "cost_insight_hourly_sweep_cycle_check", + "value": "(\"cost_insight_hourly_sweep_checkpoints\".\"cycle_id\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cycle_as_of\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cohort_created_before\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"started_at\" IS NULL) OR (\"cost_insight_hourly_sweep_checkpoints\".\"cycle_id\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cycle_as_of\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cohort_created_before\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"started_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, "public.cost_insight_notification_deliveries": { "name": "cost_insight_notification_deliveries", "schema": "", @@ -9354,6 +9487,38 @@ "concurrently": false, "method": "btree", "with": {} + }, + "IDX_cost_insight_owner_configs_user_active": { + "name": "IDX_cost_insight_owner_configs_user_active", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_configs\".\"owned_by_user_id\" IS NOT NULL AND (\"cost_insight_owner_configs\".\"spend_alerts_enabled\" = TRUE OR \"cost_insight_owner_configs\".\"cost_suggestions_enabled\" = TRUE)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_configs_org_active": { + "name": "IDX_cost_insight_owner_configs_org_active", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_configs\".\"owned_by_organization_id\" IS NOT NULL AND (\"cost_insight_owner_configs\".\"spend_alerts_enabled\" = TRUE OR \"cost_insight_owner_configs\".\"cost_suggestions_enabled\" = TRUE)", + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": { @@ -22716,6 +22881,28 @@ "concurrently": false, "method": "btree", "with": {} + }, + "idx_microdollar_usage_org_created_at": { + "name": "idx_microdollar_usage_org_created_at", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": {}, diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index d4a69a72f1..73415f71f4 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1223,8 +1223,8 @@ { "idx": 174, "version": "7", - "when": 1782500059599, - "tag": "0174_bizarre_piledriver", + "when": 1782506570342, + "tag": "0174_minor_the_call", "breakpoints": true } ] diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index c4bbd7a95e..9653dc72ad 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -2112,6 +2112,9 @@ export const microdollar_usage = pgTable( index('idx_microdollar_usage_organization_id') .on(table.organization_id) .where(isNotNull(table.organization_id)), + index('idx_microdollar_usage_org_created_at') + .on(table.organization_id, table.created_at) + .where(isNotNull(table.organization_id)), ] ); @@ -2866,6 +2869,7 @@ export const cost_insight_evaluation_dirty_owners = pgTable( dirty_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), next_attempt_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), claimed_at: timestamp({ withTimezone: true, mode: 'string' }), + claim_token: uuid(), attempt_count: integer().default(0).notNull(), last_error_redacted: text(), created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), @@ -2899,12 +2903,59 @@ export const cost_insight_evaluation_dirty_owners = pgTable( 'cost_insight_evaluation_dirty_owners_attempt_count_check', sql`${table.attempt_count} >= 0` ), + check( + 'cost_insight_evaluation_dirty_owners_claim_token_check', + sql`(${table.claimed_at} IS NULL AND ${table.claim_token} IS NULL) OR (${table.claimed_at} IS NOT NULL AND ${table.claim_token} IS NOT NULL)` + ), ] ); export type CostInsightEvaluationDirtyOwner = typeof cost_insight_evaluation_dirty_owners.$inferSelect; +export const cost_insight_hourly_sweep_checkpoints = pgTable( + 'cost_insight_hourly_sweep_checkpoints', + { + job_name: text().primaryKey().notNull(), + cycle_id: uuid(), + cycle_as_of: timestamp({ withTimezone: true, mode: 'string' }), + cohort_created_before: timestamp({ withTimezone: true, mode: 'string' }), + cursor_owner_type: text().$type<'user' | 'organization'>(), + cursor_owner_id: text(), + lease_token: uuid(), + lease_expires_at: timestamp({ withTimezone: true, mode: 'string' }), + started_at: timestamp({ withTimezone: true, mode: 'string' }), + last_completed_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + check('cost_insight_hourly_sweep_job_name_check', sql`${table.job_name} <> ''`), + check( + 'cost_insight_hourly_sweep_cursor_owner_type_check', + sql`${table.cursor_owner_type} IS NULL OR ${table.cursor_owner_type} IN ('user', 'organization')` + ), + check( + 'cost_insight_hourly_sweep_cursor_check', + sql`(${table.cursor_owner_type} IS NULL AND ${table.cursor_owner_id} IS NULL) OR (${table.cursor_owner_type} IS NOT NULL AND ${table.cursor_owner_id} IS NOT NULL)` + ), + check( + 'cost_insight_hourly_sweep_lease_check', + sql`(${table.lease_token} IS NULL AND ${table.lease_expires_at} IS NULL) OR (${table.lease_token} IS NOT NULL AND ${table.lease_expires_at} IS NOT NULL)` + ), + check( + 'cost_insight_hourly_sweep_cycle_check', + sql`(${table.cycle_id} IS NULL AND ${table.cycle_as_of} IS NULL AND ${table.cohort_created_before} IS NULL AND ${table.started_at} IS NULL) OR (${table.cycle_id} IS NOT NULL AND ${table.cycle_as_of} IS NOT NULL AND ${table.cohort_created_before} IS NOT NULL AND ${table.started_at} IS NOT NULL)` + ), + ] +); + +export type CostInsightHourlySweepCheckpoint = + typeof cost_insight_hourly_sweep_checkpoints.$inferSelect; + export const cost_insight_rollup_coverage = pgTable( 'cost_insight_rollup_coverage', { @@ -2988,48 +3039,78 @@ export const cost_insight_rollup_degraded_intervals = pgTable( export type CostInsightRollupDegradedInterval = typeof cost_insight_rollup_degraded_intervals.$inferSelect; -export type CostInsightEventSnapshot = { - thresholdMicrodollars?: number | null; - thresholdWindow?: 'rolling_24h' | 'rolling_7d' | 'rolling_30d'; - rolling24HourMicrodollars?: number | null; - rolling7DayMicrodollars?: number | null; - rolling30DayMicrodollars?: number | null; - currentHourVariableMicrodollars?: number | null; - anomalyBaselineMicrodollars?: number | null; - anomalyThresholdMicrodollars?: number | null; - topDrivers?: Array<{ - spendCategory: CostInsightSpendCategory; - source: CostInsightSpendSource; - productKey: string; - featureKey: string; - modelOrPlanKey: string; - providerKey: string; - actorUserId: string | null; - totalMicrodollars: number; - spendRecordCount: number; - }>; - topDriversWindow?: { - startInclusive: string; - endExclusive: string; - spendCategory?: CostInsightSpendCategory; - }; - changedFields?: Record; - settings?: { - spendAlertsEnabled: boolean; - anomalyAlertsEnabled: boolean; - costSuggestionsEnabled: boolean; - spendThresholdMicrodollars: number | null; - spend7DayThresholdMicrodollars: number | null; - spend30DayThresholdMicrodollars: number | null; - }; - suggestion?: { - suggestionKey: string; - evidenceWindowStart: string; - evidenceWindowEnd: string; - observedMicrodollars: number; - ctaHref: string; - }; -}; +const CostInsightSafeIntegerSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER); +const CostInsightPositiveSafeIntegerSchema = z + .number() + .int() + .positive() + .max(Number.MAX_SAFE_INTEGER); +const CostInsightTimestampSchema = z.string().refine(value => Number.isFinite(Date.parse(value)), { + message: 'Expected parseable timestamp.', +}); +const CostInsightDriverDimensionSchema = z.string().min(1).max(128); + +export const CostInsightEventSnapshotSchema = z.object({ + thresholdMicrodollars: CostInsightSafeIntegerSchema.nullable().optional(), + thresholdWindow: z.enum(['rolling_24h', 'rolling_7d', 'rolling_30d']).optional(), + rolling24HourMicrodollars: CostInsightSafeIntegerSchema.nullable().optional(), + rolling7DayMicrodollars: CostInsightSafeIntegerSchema.nullable().optional(), + rolling30DayMicrodollars: CostInsightSafeIntegerSchema.nullable().optional(), + currentHourVariableMicrodollars: CostInsightSafeIntegerSchema.nullable().optional(), + anomalyBaselineMicrodollars: CostInsightSafeIntegerSchema.nullable().optional(), + anomalyThresholdMicrodollars: CostInsightSafeIntegerSchema.nullable().optional(), + topDrivers: z + .array( + z.object({ + spendCategory: z.enum(['variable', 'scheduled']), + source: z.enum(['ai_gateway', 'kiloclaw', 'coding_plan', 'other']), + productKey: CostInsightDriverDimensionSchema, + featureKey: CostInsightDriverDimensionSchema, + modelOrPlanKey: CostInsightDriverDimensionSchema, + providerKey: CostInsightDriverDimensionSchema, + actorUserId: z.string().min(1).nullable(), + totalMicrodollars: CostInsightPositiveSafeIntegerSchema, + spendRecordCount: CostInsightPositiveSafeIntegerSchema, + }) + ) + .max(5) + .optional(), + topDriversWindow: z + .object({ + startInclusive: CostInsightTimestampSchema, + endExclusive: CostInsightTimestampSchema, + spendCategory: z.enum(['variable', 'scheduled']).optional(), + }) + .refine(value => Date.parse(value.endExclusive) > Date.parse(value.startInclusive), { + message: 'Expected topDriversWindow endExclusive to be after startInclusive.', + }) + .optional(), + changedFields: z.record(z.string(), z.object({ old: z.unknown(), new: z.unknown() })).optional(), + settings: z + .object({ + spendAlertsEnabled: z.boolean(), + anomalyAlertsEnabled: z.boolean(), + costSuggestionsEnabled: z.boolean(), + spendThresholdMicrodollars: CostInsightSafeIntegerSchema.nullable(), + spend7DayThresholdMicrodollars: CostInsightSafeIntegerSchema.nullable(), + spend30DayThresholdMicrodollars: CostInsightSafeIntegerSchema.nullable(), + }) + .optional(), + suggestion: z + .object({ + suggestionKey: z.string().regex(/^[0-9a-f]{64}$/), + evidenceWindowStart: CostInsightTimestampSchema, + evidenceWindowEnd: CostInsightTimestampSchema, + observedMicrodollars: CostInsightPositiveSafeIntegerSchema, + ctaHref: z.string().min(1), + }) + .refine(value => Date.parse(value.evidenceWindowEnd) > Date.parse(value.evidenceWindowStart), { + message: 'Expected suggestion evidence window end to be after start.', + }) + .optional(), +}); + +export type CostInsightEventSnapshot = z.infer; export const cost_insight_owner_configs = pgTable( 'cost_insight_owner_configs', @@ -3069,6 +3150,16 @@ export const cost_insight_owner_configs = pgTable( index('IDX_cost_insight_owner_configs_evaluation') .on(table.updated_at, table.id) .where(sql`${table.spend_alerts_enabled} = TRUE OR ${table.cost_suggestions_enabled} = TRUE`), + index('IDX_cost_insight_owner_configs_user_active') + .on(table.owned_by_user_id) + .where( + sql`${table.owned_by_user_id} IS NOT NULL AND (${table.spend_alerts_enabled} = TRUE OR ${table.cost_suggestions_enabled} = TRUE)` + ), + index('IDX_cost_insight_owner_configs_org_active') + .on(table.owned_by_organization_id) + .where( + sql`${table.owned_by_organization_id} IS NOT NULL AND (${table.spend_alerts_enabled} = TRUE OR ${table.cost_suggestions_enabled} = TRUE)` + ), check( 'cost_insight_owner_configs_owner_check', sql`(${table.owned_by_user_id} IS NOT NULL AND ${table.owned_by_organization_id} IS NULL) OR (${table.owned_by_user_id} IS NULL AND ${table.owned_by_organization_id} IS NOT NULL)` @@ -8587,6 +8678,7 @@ export const coding_plan_terms = pgTable( table.idempotency_key ), index('IDX_coding_plan_terms_subscription').on(table.subscription_id), + index('IDX_coding_plan_terms_credit_transaction').on(table.credit_transaction_id), enumCheck('coding_plan_terms_kind_check', table.kind, CodingPlanTermKind), ] );