From 76e6cc08a6f578a54dba1c2ba45312772a64340c Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 14 Mar 2026 22:00:12 -0500 Subject: [PATCH] feat(costs): add billing, quota, and budget control plane --- ...2026-03-14-billing-ledger-and-reporting.md | 468 + ...6-03-14-budget-policies-and-enforcement.md | 611 ++ packages/adapter-utils/src/billing.test.ts | 28 + packages/adapter-utils/src/billing.ts | 20 + packages/adapter-utils/src/index.ts | 1 + packages/adapter-utils/src/types.ts | 15 +- packages/adapters/claude-local/package.json | 4 +- .../claude-local/src/cli/quota-probe.ts | 124 + .../claude-local/src/server/execute.ts | 8 +- .../adapters/claude-local/src/server/index.ts | 4 + .../adapters/claude-local/src/server/quota.ts | 444 +- packages/adapters/codex-local/package.json | 3 +- .../codex-local/src/cli/quota-probe.ts | 112 + .../codex-local/src/server/execute.ts | 18 +- .../adapters/codex-local/src/server/index.ts | 3 + .../adapters/codex-local/src/server/quota.ts | 466 +- .../cursor-local/src/server/execute.ts | 23 +- .../gemini-local/src/server/execute.ts | 10 +- .../opencode-local/src/server/execute.ts | 7 +- .../adapters/pi-local/src/server/execute.ts | 7 +- packages/db/package.json | 1 + packages/db/src/migration-runtime.ts | 62 +- packages/db/src/migration-status.ts | 18 +- .../db/src/migrations/0031_zippy_magma.sql | 51 + .../migrations/0032_pretty_doctor_octopus.sql | 102 + .../db/src/migrations/meta/0031_snapshot.json | 7242 +++++++++++++++ .../db/src/migrations/meta/0032_snapshot.json | 7733 +++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 16 +- packages/db/src/schema/agents.ts | 2 + packages/db/src/schema/budget_incidents.ts | 41 + packages/db/src/schema/budget_policies.ts | 43 + packages/db/src/schema/cost_events.ts | 19 + packages/db/src/schema/finance_events.ts | 67 + packages/db/src/schema/index.ts | 3 + packages/db/src/schema/projects.ts | 2 + packages/shared/src/constants.ts | 72 +- packages/shared/src/index.ts | 39 + packages/shared/src/types/agent.ts | 3 + packages/shared/src/types/budget.ts | 99 + packages/shared/src/types/cost.ts | 33 + packages/shared/src/types/dashboard.ts | 6 + packages/shared/src/types/finance.ts | 60 + packages/shared/src/types/index.ts | 11 +- packages/shared/src/types/project.ts | 4 +- packages/shared/src/types/quota.ts | 4 + packages/shared/src/validators/budget.ts | 37 + packages/shared/src/validators/cost.ts | 10 +- packages/shared/src/validators/finance.ts | 34 + packages/shared/src/validators/index.ts | 12 + scripts/dev-runner.mjs | 38 +- .../companies-route-path-guard.test.ts | 3 + server/src/__tests__/costs-service.test.ts | 213 +- .../__tests__/quota-windows-service.test.ts | 56 + server/src/__tests__/quota-windows.test.ts | 294 +- server/src/routes/agents.ts | 15 + server/src/routes/companies.ts | 21 +- server/src/routes/costs.ts | 154 +- server/src/routes/issues.ts | 13 + server/src/services/agents.ts | 23 +- server/src/services/approvals.ts | 16 + server/src/services/budgets.ts | 919 ++ server/src/services/companies.ts | 2 + server/src/services/costs.ts | 188 +- server/src/services/dashboard.ts | 9 + server/src/services/finance.ts | 134 + server/src/services/heartbeat.ts | 135 +- server/src/services/index.ts | 2 + server/src/services/quota-windows.ts | 43 +- ui/src/api/budgets.ts | 20 + ui/src/api/costs.ts | 34 +- ui/src/components/AccountingModelCard.tsx | 69 + ui/src/components/ApprovalCard.tsx | 5 +- ui/src/components/ApprovalPayload.tsx | 26 +- ui/src/components/BillerSpendCard.tsx | 145 + ui/src/components/BudgetIncidentCard.tsx | 100 + ui/src/components/BudgetPolicyCard.tsx | 153 + ui/src/components/ClaudeSubscriptionPanel.tsx | 140 + ui/src/components/CodexSubscriptionPanel.tsx | 157 + ui/src/components/FinanceBillerCard.tsx | 44 + ui/src/components/FinanceKindCard.tsx | 43 + ui/src/components/FinanceTimelineCard.tsx | 71 + ui/src/components/ProviderQuotaCard.tsx | 200 +- ui/src/lib/inbox.test.ts | 6 + ui/src/lib/queryKeys.ts | 13 + ui/src/lib/utils.ts | 81 + ui/src/pages/AgentDetail.tsx | 97 +- ui/src/pages/ApprovalDetail.tsx | 8 +- ui/src/pages/Costs.tsx | 1182 ++- ui/src/pages/Dashboard.tsx | 27 +- ui/src/pages/IssueDetail.tsx | 6 +- ui/src/pages/ProjectDetail.tsx | 68 +- 91 files changed, 22406 insertions(+), 769 deletions(-) create mode 100644 doc/plans/2026-03-14-billing-ledger-and-reporting.md create mode 100644 doc/plans/2026-03-14-budget-policies-and-enforcement.md create mode 100644 packages/adapter-utils/src/billing.test.ts create mode 100644 packages/adapter-utils/src/billing.ts create mode 100644 packages/adapters/claude-local/src/cli/quota-probe.ts create mode 100644 packages/adapters/codex-local/src/cli/quota-probe.ts create mode 100644 packages/db/src/migrations/0031_zippy_magma.sql create mode 100644 packages/db/src/migrations/0032_pretty_doctor_octopus.sql create mode 100644 packages/db/src/migrations/meta/0031_snapshot.json create mode 100644 packages/db/src/migrations/meta/0032_snapshot.json create mode 100644 packages/db/src/schema/budget_incidents.ts create mode 100644 packages/db/src/schema/budget_policies.ts create mode 100644 packages/db/src/schema/finance_events.ts create mode 100644 packages/shared/src/types/budget.ts create mode 100644 packages/shared/src/types/finance.ts create mode 100644 packages/shared/src/validators/budget.ts create mode 100644 packages/shared/src/validators/finance.ts create mode 100644 server/src/__tests__/quota-windows-service.test.ts create mode 100644 server/src/services/budgets.ts create mode 100644 server/src/services/finance.ts create mode 100644 ui/src/api/budgets.ts create mode 100644 ui/src/components/AccountingModelCard.tsx create mode 100644 ui/src/components/BillerSpendCard.tsx create mode 100644 ui/src/components/BudgetIncidentCard.tsx create mode 100644 ui/src/components/BudgetPolicyCard.tsx create mode 100644 ui/src/components/ClaudeSubscriptionPanel.tsx create mode 100644 ui/src/components/CodexSubscriptionPanel.tsx create mode 100644 ui/src/components/FinanceBillerCard.tsx create mode 100644 ui/src/components/FinanceKindCard.tsx create mode 100644 ui/src/components/FinanceTimelineCard.tsx diff --git a/doc/plans/2026-03-14-billing-ledger-and-reporting.md b/doc/plans/2026-03-14-billing-ledger-and-reporting.md new file mode 100644 index 00000000..3c0b210c --- /dev/null +++ b/doc/plans/2026-03-14-billing-ledger-and-reporting.md @@ -0,0 +1,468 @@ +# Billing Ledger and Reporting + +## Context + +Paperclip currently stores model spend in `cost_events` and operational run state in `heartbeat_runs`. +That split is fine, but the current reporting code tries to infer billing semantics by mixing both tables: + +- `cost_events` knows provider, model, tokens, and dollars +- `heartbeat_runs.usage_json` knows some per-run billing metadata +- `heartbeat_runs.usage_json` does **not** currently carry enough normalized billing dimensions to support honest provider-level reporting + +This becomes incorrect as soon as a company uses more than one provider, more than one billing channel, or more than one billing mode. + +Examples: + +- direct OpenAI API usage +- Claude subscription usage with zero marginal dollars +- subscription overage with dollars and tokens +- OpenRouter billing where the biller is OpenRouter but the upstream provider is Anthropic or OpenAI + +The system needs to support: + +- dollar reporting +- token reporting +- subscription-included usage +- subscription overage +- direct metered API usage +- future aggregator billing such as OpenRouter + +## Product Decision + +`cost_events` becomes the canonical billing and usage ledger for reporting. + +`heartbeat_runs` remains an operational execution log. It may keep mirrored billing metadata for debugging and transcripts, but reporting must not reconstruct billing semantics from `heartbeat_runs.usage_json`. + +## Decision: One Ledger Or Two + +We do **not** need two tables to solve the current PR's problem. +For request-level inference reporting, `cost_events` is enough if it carries the right dimensions: + +- upstream provider +- biller +- billing type +- model +- token fields +- billed amount + +That is why the first implementation pass extends `cost_events` instead of introducing a second table immediately. + +However, if Paperclip needs to account for the full billing surface of aggregators and managed AI platforms, then `cost_events` alone is not enough. +Some charges are not cleanly representable as a single model inference event: + +- account top-ups and credit purchases +- platform fees charged at purchase time +- BYOK platform fees that are account-level or threshold-based +- prepaid credit expirations, refunds, and adjustments +- provisioned throughput commitments +- fine-tuning, training, model import, and storage charges +- gateway logging or other platform overhead that is not attributable to one prompt/response pair + +So the decision is: + +- near term: keep `cost_events` as the inference and usage ledger +- next phase: add `finance_events` for non-inference financial events + +This is a deliberate split between: + +- usage and inference accounting +- account-level and platform-level financial accounting + +That separation keeps request reporting honest without forcing us to fake invoice semantics onto rows that were never request-scoped. + +## External Motivation And Sources + +The need for this model is not theoretical. +It follows directly from the billing systems of providers and aggregators Paperclip needs to support. + +### OpenRouter + +Source URLs: + +- https://openrouter.ai/docs/faq#credit-and-billing-systems +- https://openrouter.ai/pricing + +Relevant billing behavior as of March 14, 2026: + +- OpenRouter passes through underlying inference pricing and deducts request cost from purchased credits. +- OpenRouter charges a 5.5% fee with a $0.80 minimum when purchasing credits. +- Crypto payments are charged a 5% fee. +- BYOK has its own fee model after a free request threshold. +- OpenRouter billing is aggregated at the OpenRouter account level even when the upstream provider is Anthropic, OpenAI, Google, or another provider. + +Implication for Paperclip: + +- request usage belongs in `cost_events` +- credit purchases, purchase fees, BYOK fees, refunds, and expirations belong in `finance_events` +- `biller=openrouter` must remain distinct from `provider=anthropic|openai|google|...` + +### Cloudflare AI Gateway Unified Billing + +Source URL: + +- https://developers.cloudflare.com/ai-gateway/features/unified-billing/ + +Relevant billing behavior as of March 14, 2026: + +- Unified Billing lets users call multiple upstream providers while receiving a single Cloudflare bill. +- Usage is paid from Cloudflare-loaded credits. +- Cloudflare supports manual top-ups and auto top-up thresholds. +- Spend limits can stop request processing on daily, weekly, or monthly boundaries. +- Unified Billing traffic can use Cloudflare-managed credentials rather than the user's direct provider key. + +Implication for Paperclip: + +- request usage needs `biller=cloudflare` +- upstream provider still needs to be preserved separately +- Cloudflare credit loads and related account-level events are not inference rows and should not be forced into `cost_events` +- quota and limits reporting must support biller-level controls, not just upstream provider limits + +### Amazon Bedrock + +Source URL: + +- https://aws.amazon.com/bedrock/pricing/ + +Relevant billing behavior as of March 14, 2026: + +- Bedrock supports on-demand and batch pricing. +- Bedrock pricing varies by region. +- some pricing tiers add premiums or discounts relative to standard pricing +- provisioned throughput is commitment-based rather than request-based +- custom model import uses Custom Model Units billed per minute, with monthly storage charges +- imported model copies are billed in 5-minute windows once active +- customization and fine-tuning introduce training and hosted-model charges beyond normal inference + +Implication for Paperclip: + +- normal tokenized inference fits in `cost_events` +- provisioned throughput, custom model unit charges, training, and storage charges require `finance_events` +- region and pricing tier need to be first-class dimensions in the financial model + +## Ledger Boundary + +To keep the system coherent, the table boundary should be explicit. + +### `cost_events` + +Use `cost_events` for request-scoped usage and inference charges: + +- one row per billable or usage-bearing run event +- provider/model/biller/billingType/tokens/cost +- optionally tied to `heartbeat_run_id` +- supports direct APIs, subscriptions, overage, OpenRouter-routed inference, Cloudflare-routed inference, and Bedrock on-demand inference + +### `finance_events` + +Use `finance_events` for account-scoped or platform-scoped financial events: + +- credit purchase +- top-up +- refund +- fee +- expiry +- provisioned capacity +- training +- model import +- storage +- invoice adjustment + +These rows may or may not have a related model, provider, or run id. +Trying to force them into `cost_events` would either create fake request rows or create null-heavy rows that mean something fundamentally different from inference usage. + +## Canonical Billing Dimensions + +Every persisted billing event should model four separate axes: + +1. Usage provider + The upstream provider whose model performed the work. + Examples: `openai`, `anthropic`, `google`. + +2. Biller + The system that charged for the usage. + Examples: `openai`, `anthropic`, `openrouter`, `cursor`, `chatgpt`. + +3. Billing type + The pricing mode applied to the event. + Initial canonical values: + - `metered_api` + - `subscription_included` + - `subscription_overage` + - `credits` + - `fixed` + - `unknown` + +4. Measures + Usage and billing must both be storable: + - `input_tokens` + - `output_tokens` + - `cached_input_tokens` + - `cost_cents` + +These dimensions are independent. +For example, an event may be: + +- provider: `anthropic` +- biller: `openrouter` +- billing type: `metered_api` +- tokens: non-zero +- cost cents: non-zero + +Or: + +- provider: `anthropic` +- biller: `anthropic` +- billing type: `subscription_included` +- tokens: non-zero +- cost cents: `0` + +## Schema Changes + +Extend `cost_events` with: + +- `heartbeat_run_id uuid null references heartbeat_runs.id` +- `biller text not null default 'unknown'` +- `billing_type text not null default 'unknown'` +- `cached_input_tokens int not null default 0` + +Keep `provider` as the upstream usage provider. +Do not overload `provider` to mean biller. + +Add a future `finance_events` table for account-level financial events with fields along these lines: + +- `company_id` +- `occurred_at` +- `event_kind` +- `direction` +- `biller` +- `provider nullable` +- `execution_adapter_type nullable` +- `pricing_tier nullable` +- `region nullable` +- `model nullable` +- `quantity nullable` +- `unit nullable` +- `amount_cents` +- `currency` +- `estimated` +- `related_cost_event_id nullable` +- `related_heartbeat_run_id nullable` +- `external_invoice_id nullable` +- `metadata_json nullable` + +Add indexes: + +- `(company_id, biller, occurred_at)` +- `(company_id, provider, occurred_at)` +- `(company_id, heartbeat_run_id)` if distinct-run reporting remains common + +## Shared Contract Changes + +### Shared types + +Add a shared billing type union and enrich cost types with: + +- `heartbeatRunId` +- `biller` +- `billingType` +- `cachedInputTokens` + +Update reporting response types so the provider breakdown reflects the ledger directly rather than inferred run metadata. + +### Validators + +Extend `createCostEventSchema` to accept: + +- `heartbeatRunId` +- `biller` +- `billingType` +- `cachedInputTokens` + +Defaults: + +- `biller` defaults to `provider` +- `billingType` defaults to `unknown` +- `cachedInputTokens` defaults to `0` + +## Adapter Contract Changes + +Extend adapter execution results so they can report: + +- `biller` +- richer billing type values + +Backwards compatibility: + +- existing adapter values `api` and `subscription` are treated as legacy aliases +- map `api -> metered_api` +- map `subscription -> subscription_included` + +Future adapters may emit the canonical values directly. + +OpenRouter support will use: + +- `provider` = upstream provider when known +- `biller` = `openrouter` +- `billingType` = `metered_api` unless OpenRouter later exposes another billing mode + +Cloudflare Unified Billing support will use: + +- `provider` = upstream provider when known +- `biller` = `cloudflare` +- `billingType` = `credits` or `metered_api` depending on the normalized request billing contract + +Bedrock support will use: + +- `provider` = upstream provider or `aws_bedrock` depending on adapter shape +- `biller` = `aws_bedrock` +- `billingType` = request-scoped mode for inference rows +- `finance_events` for provisioned, training, import, and storage charges + +## Write Path Changes + +### Heartbeat-created events + +When a heartbeat run produces usage or spend: + +1. normalize adapter billing metadata +2. write a ledger row to `cost_events` +3. attach `heartbeat_run_id` +4. set `provider`, `biller`, `billing_type`, token fields, and `cost_cents` + +The write path should no longer depend on later inference from `heartbeat_runs`. + +### Manual API-created events + +Manual cost event creation remains supported. +These events may have `heartbeatRunId = null`. + +Rules: + +- `provider` remains required +- `biller` defaults to `provider` +- `billingType` defaults to `unknown` + +## Reporting Changes + +### Server + +Refactor reporting queries to use `cost_events` only. + +#### `summary` + +- sum `cost_cents` + +#### `by-agent` + +- sum costs and token fields from `cost_events` +- use `count(distinct heartbeat_run_id)` filtered by billing type for run counts +- use token sums filtered by billing type for subscription usage + +#### `by-provider` + +- group by `provider`, `model` +- sum costs and token fields directly from the ledger +- derive billing-type slices from `cost_events.billing_type` +- never pro-rate from unrelated `heartbeat_runs` + +#### future `by-biller` + +- group by `biller` +- this is the right view for invoice and subscription accountability + +#### `window-spend` + +- continue to use `cost_events` + +#### project attribution + +Keep current project attribution logic for now, but prefer `cost_events.heartbeat_run_id` as the join anchor whenever possible. + +## UI Changes + +### Principles + +- Spend, usage, and quota are related but distinct +- a missing quota fetch is not the same as “no quota” +- provider and biller are different dimensions + +### Immediate UI changes + +1. Keep the current costs page structure. +2. Make the provider cards accurate by reading only ledger-backed values. +3. Show provider quota fetch errors explicitly instead of dropping them. + +### Follow-up UI direction + +The long-term board UI should expose: + +- Spend + Dollars by biller, provider, model, agent, project +- Usage + Tokens by provider, model, agent, project +- Quotas + Live provider or biller limits, credits, and reset windows +- Financial events + Credit purchases, top-ups, fees, refunds, commitments, storage, and other non-inference charges + +## Migration Plan + +Migration behavior: + +- add new non-destructive columns with defaults +- backfill existing rows: + - `biller = provider` + - `billing_type = 'unknown'` + - `cached_input_tokens = 0` + - `heartbeat_run_id = null` + +Do **not** attempt to backfill historical provider-level subscription attribution from `heartbeat_runs`. +That data was never stored with the required dimensions. + +## Testing Plan + +Add or update tests for: + +1. heartbeat-created ledger rows persist `heartbeatRunId`, `biller`, `billingType`, and cached tokens +2. legacy adapter billing values map correctly +3. provider reporting uses ledger data only +4. mixed-provider companies do not cross-attribute subscription usage +5. zero-dollar subscription usage still appears in token reporting +6. quota fetch failures render explicit UI state +7. manual cost events still validate and write correctly +8. biller reporting keeps upstream provider breakdowns separate +9. OpenRouter-style rows can show `biller=openrouter` with non-OpenRouter upstream providers +10. Cloudflare-style rows can show `biller=cloudflare` with preserved upstream provider identity +11. future `finance_events` aggregation handles non-request charges without requiring a model or run id + +## Delivery Plan + +### Step 1 + +- land the ledger contract and query rewrite +- make the current costs page correct + +### Step 2 + +- add biller-oriented reporting endpoints and UI + +### Step 3 + +- wire OpenRouter and any future aggregator adapters to the same contract + +### Step 4 + +- add `executionAdapterType` to persisted cost reporting if adapter-level grouping becomes a product requirement + +### Step 5 + +- introduce `finance_events` +- add non-inference accounting endpoints +- add UI for platform/account charges alongside inference spend and usage + +## Non-Goals For This Change + +- multi-currency support +- invoice reconciliation +- provider-specific cost estimation beyond persisted billed cost +- replacing `heartbeat_runs` as the operational run record diff --git a/doc/plans/2026-03-14-budget-policies-and-enforcement.md b/doc/plans/2026-03-14-budget-policies-and-enforcement.md new file mode 100644 index 00000000..5079ced9 --- /dev/null +++ b/doc/plans/2026-03-14-budget-policies-and-enforcement.md @@ -0,0 +1,611 @@ +# Budget Policies and Enforcement + +## Context + +Paperclip already treats budgets as a core control-plane responsibility: + +- `doc/SPEC.md` gives the Board authority to set budgets, pause agents, pause work, and override any budget. +- `doc/SPEC-implementation.md` says V1 must support monthly UTC budget windows, soft alerts, and hard auto-pause. +- the current code only partially implements that intent. + +Today the system has narrow money-budget behavior: + +- companies track `budgetMonthlyCents` and `spentMonthlyCents` +- agents track `budgetMonthlyCents` and `spentMonthlyCents` +- `cost_events` ingestion increments those counters +- when an agent exceeds its monthly budget, the agent is paused + +That leaves major product gaps: + +- no project budget model +- no approval generated when budget is hit +- no generic budget policy system +- no project pause semantics tied to budget +- no durable incident tracking to prevent duplicate alerts +- no separation between enforceable spend budgets and advisory usage quotas + +This plan defines the concrete budgeting model Paperclip should implement next. + +## Product Goals + +Paperclip should let operators: + +1. Set budgets on agents and projects. +2. Understand whether a budget is based on money or usage. +3. Be warned before a budget is exhausted. +4. Automatically pause work when a hard budget is hit. +5. Approve, raise, or resume from a budget stop using obvious UI. +6. See budget state on the dashboard, `/costs`, and scope detail pages. + +The system should make one thing very clear: + +- budgets are policy controls +- quotas are usage visibility + +They are related, but they are not the same concept. + +## Product Decisions + +### V1 Budget Defaults + +For the next implementation pass, Paperclip should enforce these defaults: + +- agent budgets are recurring monthly budgets +- project budgets are lifetime total budgets +- hard-stop enforcement uses billed dollars, not tokens +- monthly windows use UTC calendar months +- project total budgets do not reset automatically + +This gives a clean mental model: + +- agents are ongoing workers, so monthly recurring budget is natural +- projects are bounded workstreams, so lifetime cap is natural + +### Metric To Enforce First + +The first enforceable metric should be `billed_cents`. + +Reasoning: + +- it works across providers, billers, and models +- it maps directly to real financial risk +- it handles overage and metered usage consistently +- it avoids cross-provider token normalization problems +- it applies cleanly even when future finance events are not token-based + +Token budgets should not be the first hard-stop policy. +They should come later as advisory usage controls once the money-based system is solid. + +### Subscription Usage Decision + +Paperclip should separate subscription-included usage from billed spend: + +- `subscription_included` + - visible in reporting + - visible in usage summaries + - does not count against money budget +- `subscription_overage` + - visible in reporting + - counts against money budget +- `metered_api` + - visible in reporting + - counts against money budget + +This keeps the budget system honest: + +- users should not see "spend" rise for usage that did not incur marginal billed cost +- users should still see the token usage and provider quota state + +### Soft Alert Versus Hard Stop + +Paperclip should have two threshold classes: + +- soft alert + - creates visible notification state + - does not create an approval + - does not pause work +- hard stop + - pauses the affected scope automatically + - creates an approval requiring human resolution + - prevents additional heartbeats or task pickup in that scope + +Default thresholds: + +- soft alert at `80%` +- hard stop at `100%` + +These should be configurable per policy later, but they are good defaults now. + +## Scope Model + +### Supported Scope Types + +Budget policies should support: + +- `company` +- `agent` +- `project` + +This plan focuses on finishing `agent` and `project` first while preserving the existing company budget behavior. + +### Recommended V1.5 Policy Presets + +- Company + - metric: `billed_cents` + - window: `calendar_month_utc` +- Agent + - metric: `billed_cents` + - window: `calendar_month_utc` +- Project + - metric: `billed_cents` + - window: `lifetime` + +Future extensions can add: + +- token advisory policies +- daily or weekly spend windows +- provider- or biller-scoped budgets +- inherited delegated budgets down the org tree + +## Current Implementation Baseline + +The current codebase is not starting from zero, but the existing shape is too ad hoc to extend safely. + +### What Exists Today + +- company and agent monthly cents counters +- cost ingestion that updates those counters +- agent hard-stop pause on monthly budget overrun + +### What Is Missing + +- project budgets +- generic budget policy persistence +- generic threshold crossing detection +- incident deduplication per scope/window +- approval creation on hard-stop +- project execution blocking +- budget timeline and incident UI +- distinction between advisory quota and enforceable budget + +## Proposed Data Model + +### 1. `budget_policies` + +Create a new table for canonical budget definitions. + +Suggested fields: + +- `id` +- `company_id` +- `scope_type` +- `scope_id` +- `metric` +- `window_kind` +- `amount` +- `warn_percent` +- `hard_stop_enabled` +- `notify_enabled` +- `is_active` +- `created_by_user_id` +- `updated_by_user_id` +- `created_at` +- `updated_at` + +Notes: + +- `scope_type` is one of `company | agent | project` +- `scope_id` is nullable only for company-level policy if company is implied; otherwise keep it explicit +- `metric` should start with `billed_cents` +- `window_kind` starts with `calendar_month_utc | lifetime` +- `amount` is stored in the natural unit of the metric + +### 2. `budget_incidents` + +Create a durable record of threshold crossings. + +Suggested fields: + +- `id` +- `company_id` +- `policy_id` +- `scope_type` +- `scope_id` +- `metric` +- `window_kind` +- `window_start` +- `window_end` +- `threshold_type` +- `amount_limit` +- `amount_observed` +- `status` +- `approval_id` nullable +- `activity_id` nullable +- `resolved_at` nullable +- `created_at` +- `updated_at` + +Notes: + +- `threshold_type`: `soft | hard` +- `status`: `open | acknowledged | resolved | dismissed` +- one open incident per policy per threshold per window prevents duplicate approvals and alert spam + +### 3. Project Pause State + +Projects need explicit pause semantics. + +Recommended approach: + +- extend project status or add a pause field so a project can be blocked by budget +- preserve whether the project is paused due to budget versus manually paused + +Preferred shape: + +- keep project workflow status as-is +- add execution-state fields: + - `execution_status`: `active | paused | archived` + - `pause_reason`: `manual | budget | system | null` + +If that is too large for the immediate pass, a smaller version is: + +- add `paused_at` +- add `pause_reason` + +The key requirement is behavioral, not cosmetic: +Paperclip must know that a project is budget-paused and enforce it. + +### 4. Compatibility With Existing Budget Columns + +Existing company and agent monthly budget columns should remain temporarily for compatibility. + +Migration plan: + +1. keep reading existing columns during transition +2. create equivalent `budget_policies` rows +3. switch enforcement and UI to policies +4. later remove or deprecate legacy columns + +## Budget Engine + +Budget enforcement should move into a dedicated service. + +Current logic is buried inside cost ingestion. +That is too narrow because budget checks must apply at more than one execution boundary. + +### Responsibilities + +New service: `budgetService` + +Responsibilities: + +- resolve applicable policies for a cost event +- compute current window totals +- detect threshold crossings +- create incidents, activities, and approvals +- pause affected scopes on hard-stop +- provide preflight enforcement checks for execution entry points + +### Canonical Evaluation Flow + +When a new `cost_event` is written: + +1. persist the `cost_event` +2. identify affected scopes + - company + - agent + - project +3. fetch active policies for those scopes +4. compute current observed amount for each policy window +5. compare to thresholds +6. create soft incident if soft threshold crossed for first time in window +7. create hard incident if hard threshold crossed for first time in window +8. if hard incident: + - pause the scope + - create approval + - create activity event + - emit notification state + +### Preflight Enforcement Checks + +Budget enforcement cannot rely only on post-hoc cost ingestion. + +Paperclip must also block execution before new work starts. + +Add budget checks to: + +- scheduler heartbeat dispatch +- manual invoke endpoints +- assignment-driven wakeups +- queued run promotion +- issue checkout or pickup paths where applicable + +If a scope is budget-paused: + +- do not start a new heartbeat +- do not let the agent pick up additional work +- present a clear reason in API and UI + +### Active Run Behavior + +When a hard-stop is triggered while a run is already active: + +- mark scope paused immediately for future work +- request graceful cancellation of the current run +- allow normal cancellation timeout behavior +- write activity explaining that pause came from budget enforcement + +This mirrors the general pause semantics already expected by the product. + +## Approval Model + +Budget hard-stops should create a first-class approval. + +### New Approval Type + +Add approval type: + +- `budget_override_required` + +Payload should include: + +- `scopeType` +- `scopeId` +- `scopeName` +- `metric` +- `windowKind` +- `thresholdType` +- `budgetAmount` +- `observedAmount` +- `windowStart` +- `windowEnd` +- `topDrivers` +- `paused` + +### Resolution Actions + +The approval UI should support: + +- raise budget and resume +- resume once without changing policy +- keep paused + +Optional later action: + +- disable budget policy + +### Soft Alerts Do Not Need Approval + +Soft alerts should create: + +- activity event +- dashboard alert +- inbox notification or similar board-visible signal + +They should not create an approval by default. + +## Notification And Activity Model + +Budget events need obvious operator visibility. + +Required outputs: + +- activity log entry on threshold crossings +- dashboard surface for active budget incidents +- detail page banner on paused agent or project +- `/costs` summary of active incidents and policy health + +Later channels: + +- email +- webhook +- Slack or other integrations + +## API Plan + +### Policy Management + +Add routes for: + +- list budget policies for company +- create budget policy +- update budget policy +- archive or disable budget policy + +### Incident Surfaces + +Add routes for: + +- list active budget incidents +- list incident history +- get incident detail for a scope + +### Approval Resolution + +Budget approvals should use the existing approval system once the new approval type is added. + +Expected flows: + +- create approval on hard-stop +- resolve approval by changing policy and resuming +- resolve approval by resuming once + +### Execution Errors + +When work is blocked by budget, the API should return explicit errors. + +Examples: + +- agent invocation blocked because agent budget is paused +- issue execution blocked because project budget is paused + +Do not silently no-op. + +## UI Plan + +Budgeting should be visible in the places where operators make decisions. + +### `/costs` + +Add a budget section that includes: + +- active budget incidents +- policy list with scope, window, metric, and threshold state +- progress bars for current period or total +- clear distinction between: + - spend budget + - subscription quota +- quick actions: + - raise budget + - open approval + - resume scope if permitted + +The page should make this visual distinction obvious: + +- Budget + - enforceable spend policy +- Quota + - provider or subscription usage window + +### Agent Detail + +Add an agent budget card: + +- monthly budget amount +- current month spend +- remaining spend +- status +- warning or paused banner +- link to approval if blocked + +### Project Detail + +Add a project budget card: + +- total budget amount +- total spend to date +- remaining spend +- pause status +- approval link + +Project detail should also show if issue execution is blocked because the project is budget-paused. + +### Dashboard + +Add a high-signal budget section: + +- active budget breaches +- upcoming soft alerts +- counts of paused agents and paused projects due to budget + +The operator should not have to visit `/costs` to learn that work has stopped. + +## Budget Math + +### What Counts Toward Budget + +For V1.5 enforcement, include: + +- `metered_api` cost events +- `subscription_overage` cost events +- any future request-scoped cost event with non-zero billed cents + +Do not include: + +- `subscription_included` cost events with zero billed cents +- advisory quota rows +- account-level finance events unless and until company-level financial budgets are added explicitly + +### Why Not Tokens First + +Token budgets should not be the first hard-stop because: + +- providers count tokens differently +- cached tokens complicate simple totals +- some future charges are not token-based +- subscription tokens do not necessarily imply spend +- money remains the cleanest cross-provider enforcement metric + +### Future Budget Metrics + +Future policy metrics can include: + +- `total_tokens` +- `input_tokens` +- `output_tokens` +- `requests` +- `finance_amount_cents` + +But they should enter only after the money-budget path is stable. + +## Migration Plan + +### Phase 1: Foundation + +- add `budget_policies` +- add `budget_incidents` +- add new approval type +- add project pause metadata + +### Phase 2: Compatibility + +- backfill policies from existing company and agent monthly budget columns +- keep legacy columns readable during migration + +### Phase 3: Enforcement + +- move budget logic into dedicated service +- add hard-stop incident creation +- add activity and approval creation +- add execution guards on heartbeat and invoke paths + +### Phase 4: UI + +- `/costs` budget section +- agent detail budget card +- project detail budget card +- dashboard incident summary + +### Phase 5: Cleanup + +- move all reads/writes to `budget_policies` +- reduce legacy column reliance +- decide whether to remove old budget columns + +## Tests + +Required coverage: + +- agent monthly budget soft alert at 80% +- agent monthly budget hard-stop at 100% +- project lifetime budget soft alert +- project lifetime budget hard-stop +- `subscription_included` usage does not consume money budget +- `subscription_overage` does consume money budget +- hard-stop creates one incident per threshold per window +- hard-stop creates approval and pauses correct scope +- paused project blocks new issue execution +- paused agent blocks new heartbeat dispatch +- policy update and resume clears or resolves active incident correctly +- dashboard and `/costs` surface active incidents + +## Open Questions + +These should be explicitly deferred unless they block implementation: + +- Should project budgets also support monthly mode, or is lifetime enough for the first release? +- Should company-level budgets eventually include `finance_events` such as OpenRouter top-up fees and Bedrock provisioned charges? +- Should delegated budget editing be limited by org hierarchy in V1, or remain board-only in the UI even if the data model can support delegation later? +- Do we need "resume once" immediately, or can first approval resolution be "raise budget and resume" plus "keep paused"? + +## Recommendation + +Implement the first coherent budgeting system with these rules: + +- Agent budget = monthly billed dollars +- Project budget = lifetime billed dollars +- Hard-stop = auto-pause + approval +- Soft alert = visible warning, no approval +- Subscription usage = visible quota and token reporting, not money-budget enforcement + +This solves the real operator problem without mixing together spend control, provider quota windows, and token accounting. diff --git a/packages/adapter-utils/src/billing.test.ts b/packages/adapter-utils/src/billing.test.ts new file mode 100644 index 00000000..1b85340f --- /dev/null +++ b/packages/adapter-utils/src/billing.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { inferOpenAiCompatibleBiller } from "./billing.js"; + +describe("inferOpenAiCompatibleBiller", () => { + it("returns openrouter when OPENROUTER_API_KEY is present", () => { + expect( + inferOpenAiCompatibleBiller({ OPENROUTER_API_KEY: "sk-or-123" } as NodeJS.ProcessEnv, "openai"), + ).toBe("openrouter"); + }); + + it("returns openrouter when OPENAI_BASE_URL points at OpenRouter", () => { + expect( + inferOpenAiCompatibleBiller( + { OPENAI_BASE_URL: "https://openrouter.ai/api/v1" } as NodeJS.ProcessEnv, + "openai", + ), + ).toBe("openrouter"); + }); + + it("returns fallback when no OpenRouter markers are present", () => { + expect( + inferOpenAiCompatibleBiller( + { OPENAI_BASE_URL: "https://api.openai.com/v1" } as NodeJS.ProcessEnv, + "openai", + ), + ).toBe("openai"); + }); +}); diff --git a/packages/adapter-utils/src/billing.ts b/packages/adapter-utils/src/billing.ts new file mode 100644 index 00000000..3ecd8ec3 --- /dev/null +++ b/packages/adapter-utils/src/billing.ts @@ -0,0 +1,20 @@ +function readEnv(env: NodeJS.ProcessEnv, key: string): string | null { + const value = env[key]; + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +export function inferOpenAiCompatibleBiller( + env: NodeJS.ProcessEnv, + fallback: string | null = "openai", +): string | null { + const explicitOpenRouterKey = readEnv(env, "OPENROUTER_API_KEY"); + if (explicitOpenRouterKey) return "openrouter"; + + const baseUrl = + readEnv(env, "OPENAI_BASE_URL") ?? + readEnv(env, "OPENAI_API_BASE") ?? + readEnv(env, "OPENAI_API_BASE_URL"); + if (baseUrl && /openrouter\.ai/i.test(baseUrl)) return "openrouter"; + + return fallback; +} diff --git a/packages/adapter-utils/src/index.ts b/packages/adapter-utils/src/index.ts index 7fde4d1d..cc3cd7e0 100644 --- a/packages/adapter-utils/src/index.ts +++ b/packages/adapter-utils/src/index.ts @@ -30,3 +30,4 @@ export { redactHomePathUserSegmentsInValue, redactTranscriptEntryPaths, } from "./log-redaction.js"; +export { inferOpenAiCompatibleBiller } from "./billing.js"; diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index c9c7113f..ade4648a 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -30,7 +30,15 @@ export interface UsageSummary { cachedInputTokens?: number; } -export type AdapterBillingType = "api" | "subscription" | "unknown"; +export type AdapterBillingType = + | "api" + | "subscription" + | "metered_api" + | "subscription_included" + | "subscription_overage" + | "credits" + | "fixed" + | "unknown"; export interface AdapterRuntimeServiceReport { id?: string | null; @@ -68,6 +76,7 @@ export interface AdapterExecutionResult { sessionParams?: Record | null; sessionDisplayId?: string | null; provider?: string | null; + biller?: string | null; model?: string | null; billingType?: AdapterBillingType | null; costUsd?: number | null; @@ -185,12 +194,16 @@ export interface QuotaWindow { resetsAt: string | null; /** free-form value label for credit-style windows, e.g. "$4.20 remaining" */ valueLabel: string | null; + /** optional supporting text, e.g. reset details or provider-specific notes */ + detail?: string | null; } /** result for one provider from getQuotaWindows() */ export interface ProviderQuotaResult { /** provider slug, e.g. "anthropic", "openai" */ provider: string; + /** source label when the provider reports where the quota data came from */ + source?: string | null; /** true when the fetch succeeded and windows is populated */ ok: boolean; /** error message when ok is false */ diff --git a/packages/adapters/claude-local/package.json b/packages/adapters/claude-local/package.json index 35a6d9ed..d6dd0b7f 100644 --- a/packages/adapters/claude-local/package.json +++ b/packages/adapters/claude-local/package.json @@ -38,7 +38,9 @@ "scripts": { "build": "tsc", "clean": "rm -rf dist", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "probe:quota": "pnpm exec tsx src/cli/quota-probe.ts --json", + "probe:quota:raw": "pnpm exec tsx src/cli/quota-probe.ts --json --raw-cli" }, "dependencies": { "@paperclipai/adapter-utils": "workspace:*", diff --git a/packages/adapters/claude-local/src/cli/quota-probe.ts b/packages/adapters/claude-local/src/cli/quota-probe.ts new file mode 100644 index 00000000..09d415be --- /dev/null +++ b/packages/adapters/claude-local/src/cli/quota-probe.ts @@ -0,0 +1,124 @@ +#!/usr/bin/env node + +import { + captureClaudeCliUsageText, + fetchClaudeCliQuota, + fetchClaudeQuota, + getQuotaWindows, + parseClaudeCliUsageText, + readClaudeAuthStatus, + readClaudeToken, +} from "../server/quota.js"; + +interface ProbeArgs { + json: boolean; + includeRawCli: boolean; + oauthOnly: boolean; + cliOnly: boolean; +} + +function parseArgs(argv: string[]): ProbeArgs { + return { + json: argv.includes("--json"), + includeRawCli: argv.includes("--raw-cli"), + oauthOnly: argv.includes("--oauth-only"), + cliOnly: argv.includes("--cli-only"), + }; +} + +function stringifyError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.oauthOnly && args.cliOnly) { + throw new Error("Choose either --oauth-only or --cli-only, not both."); + } + + const authStatus = await readClaudeAuthStatus(); + const token = await readClaudeToken(); + + const result: Record = { + timestamp: new Date().toISOString(), + authStatus, + tokenAvailable: token != null, + }; + + if (!args.cliOnly) { + if (!token) { + result.oauth = { + ok: false, + error: "No Claude OAuth access token found in local credentials files.", + windows: [], + }; + } else { + try { + result.oauth = { + ok: true, + windows: await fetchClaudeQuota(token), + }; + } catch (error) { + result.oauth = { + ok: false, + error: stringifyError(error), + windows: [], + }; + } + } + } + + if (!args.oauthOnly) { + try { + const rawCliText = args.includeRawCli ? await captureClaudeCliUsageText() : null; + const windows = rawCliText ? parseClaudeCliUsageText(rawCliText) : await fetchClaudeCliQuota(); + result.cli = rawCliText + ? { + ok: true, + windows, + rawText: rawCliText, + } + : { + ok: true, + windows, + }; + } catch (error) { + result.cli = { + ok: false, + error: stringifyError(error), + windows: [], + }; + } + } + + if (!args.oauthOnly && !args.cliOnly) { + try { + result.aggregated = await getQuotaWindows(); + } catch (error) { + result.aggregated = { + ok: false, + error: stringifyError(error), + }; + } + } + + const oauthOk = (result.oauth as { ok?: boolean } | undefined)?.ok === true; + const cliOk = (result.cli as { ok?: boolean } | undefined)?.ok === true; + const aggregatedOk = (result.aggregated as { ok?: boolean } | undefined)?.ok === true; + const ok = oauthOk || cliOk || aggregatedOk; + + if (args.json || process.stdout.isTTY === false) { + console.log(JSON.stringify({ ok, ...result }, null, 2)); + } else { + console.log(`timestamp: ${result.timestamp}`); + console.log(`auth: ${JSON.stringify(authStatus)}`); + console.log(`tokenAvailable: ${token != null}`); + if (result.oauth) console.log(`oauth: ${JSON.stringify(result.oauth, null, 2)}`); + if (result.cli) console.log(`cli: ${JSON.stringify(result.cli, null, 2)}`); + if (result.aggregated) console.log(`aggregated: ${JSON.stringify(result.aggregated, null, 2)}`); + } + + if (!ok) process.exitCode = 1; +} + +await main(); diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index dfcd1173..cd1f0f15 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -340,7 +340,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise typeof entry[1] === "string", + ), + ); + const billingType = resolveClaudeBillingType(effectiveEnv); const skillsDir = await buildSkillsDir(); // When instructionsFilePath is configured, create a combined temp file that @@ -547,6 +552,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) return fromEnv.trim(); return path.join(os.homedir(), ".claude"); } -export async function readClaudeToken(): Promise { - const credPath = path.join(claudeConfigDir(), "credentials.json"); +function hasNonEmptyProcessEnv(key: string): boolean { + const value = process.env[key]; + return typeof value === "string" && value.trim().length > 0; +} + +function createClaudeQuotaEnv(): Record { + const env: Record = {}; + for (const [key, value] of Object.entries(process.env)) { + if (typeof value !== "string") continue; + if (key.startsWith("ANTHROPIC_")) continue; + env[key] = value; + } + return env; +} + +function stripBackspaces(text: string): string { + let out = ""; + for (const char of text) { + if (char === "\b") { + out = out.slice(0, -1); + } else { + out += char; + } + } + return out; +} + +function stripAnsi(text: string): string { + return text + .replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/g, "") + .replace(/\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, ""); +} + +function cleanTerminalText(text: string): string { + return stripAnsi(stripBackspaces(text)) + .replace(/\u0000/g, "") + .replace(/\r/g, "\n"); +} + +function normalizeForLabelSearch(text: string): string { + return text.toLowerCase().replace(/[^a-z0-9]+/g, ""); +} + +function trimToLatestUsagePanel(text: string): string | null { + const lower = text.toLowerCase(); + const settingsIndex = lower.lastIndexOf("settings:"); + if (settingsIndex < 0) return null; + let tail = text.slice(settingsIndex); + const tailLower = tail.toLowerCase(); + if (!tailLower.includes("usage")) return null; + if (!tailLower.includes("current session") && !tailLower.includes("loading usage")) return null; + const stopMarkers = [ + "status dialog dismissed", + "checking for updates", + "press ctrl-c again to exit", + ]; + let stopIndex = -1; + for (const marker of stopMarkers) { + const markerIndex = tailLower.indexOf(marker); + if (markerIndex >= 0 && (stopIndex === -1 || markerIndex < stopIndex)) { + stopIndex = markerIndex; + } + } + if (stopIndex >= 0) { + tail = tail.slice(0, stopIndex); + } + return tail; +} + +async function readClaudeTokenFromFile(credPath: string): Promise { let raw: string; try { raw = await fs.readFile(credPath, "utf8"); @@ -31,22 +106,93 @@ export async function readClaudeToken(): Promise { return typeof token === "string" && token.length > 0 ? token : null; } +interface ClaudeAuthStatus { + loggedIn: boolean; + authMethod: string | null; + subscriptionType: string | null; +} + +export async function readClaudeAuthStatus(): Promise { + try { + const { stdout } = await execFileAsync("claude", ["auth", "status"], { + env: process.env, + timeout: 5_000, + maxBuffer: 1024 * 1024, + }); + const parsed = JSON.parse(stdout) as Record; + return { + loggedIn: parsed.loggedIn === true, + authMethod: typeof parsed.authMethod === "string" ? parsed.authMethod : null, + subscriptionType: typeof parsed.subscriptionType === "string" ? parsed.subscriptionType : null, + }; + } catch { + return null; + } +} + +function describeClaudeSubscriptionAuth(status: ClaudeAuthStatus | null): string | null { + if (!status?.loggedIn || status.authMethod !== "claude.ai") return null; + return status.subscriptionType + ? `Claude is logged in via claude.ai (${status.subscriptionType})` + : "Claude is logged in via claude.ai"; +} + +export async function readClaudeToken(): Promise { + const configDir = claudeConfigDir(); + for (const filename of [".credentials.json", "credentials.json"]) { + const token = await readClaudeTokenFromFile(path.join(configDir, filename)); + if (token) return token; + } + return null; +} + interface AnthropicUsageWindow { utilization?: number | null; resets_at?: string | null; } +interface AnthropicExtraUsage { + is_enabled?: boolean | null; + monthly_limit?: number | null; + used_credits?: number | null; + utilization?: number | null; + currency?: string | null; +} + interface AnthropicUsageResponse { five_hour?: AnthropicUsageWindow | null; seven_day?: AnthropicUsageWindow | null; seven_day_sonnet?: AnthropicUsageWindow | null; seven_day_opus?: AnthropicUsageWindow | null; + extra_usage?: AnthropicExtraUsage | null; +} + +function formatCurrencyAmount(value: number, currency: string | null | undefined): string { + const code = typeof currency === "string" && currency.trim().length > 0 ? currency.trim().toUpperCase() : "USD"; + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: code, + maximumFractionDigits: 2, + }).format(value); +} + +function formatExtraUsageLabel(extraUsage: AnthropicExtraUsage): string | null { + const monthlyLimit = extraUsage.monthly_limit; + const usedCredits = extraUsage.used_credits; + if ( + typeof monthlyLimit !== "number" || + !Number.isFinite(monthlyLimit) || + typeof usedCredits !== "number" || + !Number.isFinite(usedCredits) + ) { + return null; + } + return `${formatCurrencyAmount(usedCredits, extraUsage.currency)} / ${formatCurrencyAmount(monthlyLimit, extraUsage.currency)}`; } /** Convert a 0-1 utilization fraction to a 0-100 integer percent. Returns null for null/undefined input. */ export function toPercent(utilization: number | null | undefined): number | null { if (utilization == null) return null; - // utilization is 0-1 fraction; clamp to 100 in case of floating-point overshoot return Math.min(100, Math.round(utilization * 100)); } @@ -64,7 +210,7 @@ export async function fetchWithTimeout(url: string, init: RequestInit, ms = 8000 export async function fetchClaudeQuota(token: string): Promise { const resp = await fetchWithTimeout("https://api.anthropic.com/api/oauth/usage", { headers: { - "Authorization": `Bearer ${token}`, + Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20", }, }); @@ -74,44 +220,312 @@ export async function fetchClaudeQuota(token: string): Promise { if (body.five_hour != null) { windows.push({ - label: "5h", + label: "Current session", usedPercent: toPercent(body.five_hour.utilization), resetsAt: body.five_hour.resets_at ?? null, valueLabel: null, + detail: null, }); } if (body.seven_day != null) { windows.push({ - label: "7d", + label: "Current week (all models)", usedPercent: toPercent(body.seven_day.utilization), resetsAt: body.seven_day.resets_at ?? null, valueLabel: null, + detail: null, }); } if (body.seven_day_sonnet != null) { windows.push({ - label: "Sonnet 7d", + label: "Current week (Sonnet only)", usedPercent: toPercent(body.seven_day_sonnet.utilization), resetsAt: body.seven_day_sonnet.resets_at ?? null, valueLabel: null, + detail: null, }); } if (body.seven_day_opus != null) { windows.push({ - label: "Opus 7d", + label: "Current week (Opus only)", usedPercent: toPercent(body.seven_day_opus.utilization), resetsAt: body.seven_day_opus.resets_at ?? null, valueLabel: null, + detail: null, + }); + } + if (body.extra_usage != null) { + windows.push({ + label: "Extra usage", + usedPercent: body.extra_usage.is_enabled === false ? null : toPercent(body.extra_usage.utilization), + resetsAt: null, + valueLabel: + body.extra_usage.is_enabled === false + ? "Not enabled" + : formatExtraUsageLabel(body.extra_usage), + detail: + body.extra_usage.is_enabled === false + ? "Extra usage not enabled" + : "Monthly extra usage pool", }); } return windows; } -export async function getQuotaWindows(): Promise { - const token = await readClaudeToken(); - if (!token) { - return { provider: "anthropic", ok: false, error: "no local claude auth token", windows: [] }; - } - const windows = await fetchClaudeQuota(token); - return { provider: "anthropic", ok: true, windows }; +function usageOutputLooksRelevant(text: string): boolean { + const normalized = normalizeForLabelSearch(text); + return normalized.includes("currentsession") + || normalized.includes("currentweek") + || normalized.includes("loadingusage") + || normalized.includes("failedtoloadusagedata") + || normalized.includes("tokenexpired") + || normalized.includes("authenticationerror") + || normalized.includes("ratelimited"); +} + +function usageOutputLooksComplete(text: string): boolean { + const normalized = normalizeForLabelSearch(text); + if ( + normalized.includes("failedtoloadusagedata") + || normalized.includes("tokenexpired") + || normalized.includes("authenticationerror") + || normalized.includes("ratelimited") + ) { + return true; + } + return normalized.includes("currentsession") + && (normalized.includes("currentweek") || normalized.includes("extrausage")) + && /[0-9]{1,3}(?:\.[0-9]+)?%/i.test(text); +} + +function extractUsageError(text: string): string | null { + const lower = text.toLowerCase(); + const compact = lower.replace(/\s+/g, ""); + if (lower.includes("token_expired") || lower.includes("token has expired")) { + return "Claude CLI token expired. Run `claude login` to refresh."; + } + if (lower.includes("authentication_error")) { + return "Claude CLI authentication error. Run `claude login`."; + } + if (lower.includes("rate_limit_error") || lower.includes("rate limited") || compact.includes("ratelimited")) { + return "Claude CLI usage endpoint is rate limited right now. Please try again later."; + } + if (lower.includes("failed to load usage data") || compact.includes("failedtoloadusagedata")) { + return "Claude CLI could not load usage data. Open the CLI and retry `/usage`."; + } + return null; +} + +function percentFromLine(line: string): number | null { + const match = line.match(/([0-9]{1,3}(?:\.[0-9]+)?)\s*%/i); + if (!match) return null; + const rawValue = Number(match[1]); + if (!Number.isFinite(rawValue)) return null; + const clamped = Math.min(100, Math.max(0, rawValue)); + const lower = line.toLowerCase(); + if (lower.includes("remaining") || lower.includes("left") || lower.includes("available")) { + return Math.max(0, Math.min(100, Math.round(100 - clamped))); + } + return Math.round(clamped); +} + +function isQuotaLabel(line: string): boolean { + const normalized = normalizeForLabelSearch(line); + return normalized === "currentsession" + || normalized === "currentweekallmodels" + || normalized === "currentweeksonnetonly" + || normalized === "currentweeksonnet" + || normalized === "currentweekopusonly" + || normalized === "currentweekopus" + || normalized === "extrausage"; +} + +function canonicalQuotaLabel(line: string): string { + switch (normalizeForLabelSearch(line)) { + case "currentsession": + return "Current session"; + case "currentweekallmodels": + return "Current week (all models)"; + case "currentweeksonnetonly": + case "currentweeksonnet": + return "Current week (Sonnet only)"; + case "currentweekopusonly": + case "currentweekopus": + return "Current week (Opus only)"; + case "extrausage": + return "Extra usage"; + default: + return line; + } +} + +function formatClaudeCliDetail(label: string, lines: string[]): string | null { + const normalizedLabel = normalizeForLabelSearch(label); + if (normalizedLabel === "extrausage") { + const compact = lines.join(" ").replace(/\s+/g, "").toLowerCase(); + if (compact.includes("extrausagenotenabled")) { + return "Extra usage not enabled • /extra-usage to enable"; + } + const firstLine = lines.find((line) => line.trim().length > 0) ?? null; + return firstLine; + } + + const resetLine = lines.find((line) => /^resets/i.test(line) || normalizeForLabelSearch(line).startsWith("resets")); + if (!resetLine) return null; + return resetLine + .replace(/^Resets/i, "Resets ") + .replace(/([A-Z][a-z]{2})(\d)/g, "$1 $2") + .replace(/(\d)at(\d)/g, "$1 at $2") + .replace(/(am|pm)\(/gi, "$1 (") + .replace(/([A-Za-z])\(/g, "$1 (") + .replace(/\s+/g, " ") + .trim(); +} + +export function parseClaudeCliUsageText(text: string): QuotaWindow[] { + const cleaned = trimToLatestUsagePanel(cleanTerminalText(text)) ?? cleanTerminalText(text); + const usageError = extractUsageError(cleaned); + if (usageError) throw new Error(usageError); + + const lines = cleaned + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); + + const sections: Array<{ label: string; lines: string[] }> = []; + let current: { label: string; lines: string[] } | null = null; + + for (const line of lines) { + if (isQuotaLabel(line)) { + if (current) sections.push(current); + current = { label: canonicalQuotaLabel(line), lines: [] }; + continue; + } + if (current) current.lines.push(line); + } + if (current) sections.push(current); + + const windows = sections.map((section) => { + const usedPercent = section.lines.map(percentFromLine).find((value) => value != null) ?? null; + return { + label: section.label, + usedPercent, + resetsAt: null, + valueLabel: null, + detail: formatClaudeCliDetail(section.label, section.lines), + }; + }); + + if (!windows.some((window) => normalizeForLabelSearch(window.label) === "currentsession")) { + throw new Error("Could not parse Claude CLI usage output."); + } + return windows; +} + +function quoteForShell(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function buildClaudeCliShellProbeCommand(): string { + const feed = "(sleep 2; printf '/usage\\r'; sleep 6; printf '\\033'; sleep 1; printf '\\003')"; + const claudeCommand = "claude --tools \"\""; + if (process.platform === "darwin") { + return `${feed} | script -q /dev/null ${claudeCommand}`; + } + return `${feed} | script -q -e -f -c ${quoteForShell(claudeCommand)} /dev/null`; +} + +export async function captureClaudeCliUsageText(timeoutMs = 12_000): Promise { + const command = buildClaudeCliShellProbeCommand(); + try { + const { stdout, stderr } = await execFileAsync("sh", ["-c", command], { + env: createClaudeQuotaEnv(), + timeout: timeoutMs, + maxBuffer: 8 * 1024 * 1024, + }); + const output = `${stdout}${stderr}`; + const cleaned = cleanTerminalText(output); + if (usageOutputLooksComplete(cleaned)) return output; + throw new Error("Claude CLI usage probe ended before rendering usage."); + } catch (error) { + const stdout = + typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string" + ? error.stdout + : ""; + const stderr = + typeof error === "object" && error !== null && "stderr" in error && typeof error.stderr === "string" + ? error.stderr + : ""; + const output = `${stdout}${stderr}`; + const cleaned = cleanTerminalText(output); + if (usageOutputLooksComplete(cleaned)) return output; + if (usageOutputLooksRelevant(cleaned)) { + throw new Error("Claude CLI usage probe ended before rendering usage."); + } + throw error instanceof Error ? error : new Error(String(error)); + } +} + +export async function fetchClaudeCliQuota(): Promise { + const rawText = await captureClaudeCliUsageText(); + return parseClaudeCliUsageText(rawText); +} + +function formatProviderError(source: string, error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + return `${source}: ${message}`; +} + +export async function getQuotaWindows(): Promise { + const authStatus = await readClaudeAuthStatus(); + const authDescription = describeClaudeSubscriptionAuth(authStatus); + const token = await readClaudeToken(); + + const errors: string[] = []; + + if (token) { + try { + const windows = await fetchClaudeQuota(token); + return { provider: "anthropic", source: CLAUDE_USAGE_SOURCE_OAUTH, ok: true, windows }; + } catch (error) { + errors.push(formatProviderError("Anthropic OAuth usage", error)); + } + } + + try { + const windows = await fetchClaudeCliQuota(); + return { provider: "anthropic", source: CLAUDE_USAGE_SOURCE_CLI, ok: true, windows }; + } catch (error) { + errors.push(formatProviderError("Claude CLI /usage", error)); + } + + if (hasNonEmptyProcessEnv("ANTHROPIC_API_KEY") && !authDescription) { + return { + provider: "anthropic", + ok: false, + error: + errors[0] + ?? "ANTHROPIC_API_KEY is set and no local Claude subscription session is available for quota polling", + windows: [], + }; + } + + if (authDescription) { + return { + provider: "anthropic", + ok: false, + error: + errors.length > 0 + ? `${authDescription}, but quota polling failed (${errors.join("; ")})` + : `${authDescription}, but Paperclip could not load subscription quota data`, + windows: [], + }; + } + + return { + provider: "anthropic", + ok: false, + error: errors[0] ?? "no local claude auth token", + windows: [], + }; } diff --git a/packages/adapters/codex-local/package.json b/packages/adapters/codex-local/package.json index 4b28c729..0755b214 100644 --- a/packages/adapters/codex-local/package.json +++ b/packages/adapters/codex-local/package.json @@ -38,7 +38,8 @@ "scripts": { "build": "tsc", "clean": "rm -rf dist", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "probe:quota": "pnpm exec tsx src/cli/quota-probe.ts --json" }, "dependencies": { "@paperclipai/adapter-utils": "workspace:*", diff --git a/packages/adapters/codex-local/src/cli/quota-probe.ts b/packages/adapters/codex-local/src/cli/quota-probe.ts new file mode 100644 index 00000000..3b890414 --- /dev/null +++ b/packages/adapters/codex-local/src/cli/quota-probe.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +import { + fetchCodexQuota, + fetchCodexRpcQuota, + getQuotaWindows, + readCodexAuthInfo, + readCodexToken, +} from "../server/quota.js"; + +interface ProbeArgs { + json: boolean; + rpcOnly: boolean; + whamOnly: boolean; +} + +function parseArgs(argv: string[]): ProbeArgs { + return { + json: argv.includes("--json"), + rpcOnly: argv.includes("--rpc-only"), + whamOnly: argv.includes("--wham-only"), + }; +} + +function stringifyError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.rpcOnly && args.whamOnly) { + throw new Error("Choose either --rpc-only or --wham-only, not both."); + } + + const auth = await readCodexAuthInfo(); + const token = await readCodexToken(); + + const result: Record = { + timestamp: new Date().toISOString(), + auth, + tokenAvailable: token != null, + }; + + if (!args.whamOnly) { + try { + result.rpc = { + ok: true, + ...(await fetchCodexRpcQuota()), + }; + } catch (error) { + result.rpc = { + ok: false, + error: stringifyError(error), + windows: [], + }; + } + } + + if (!args.rpcOnly) { + if (!token) { + result.wham = { + ok: false, + error: "No local Codex auth token found in ~/.codex/auth.json.", + windows: [], + }; + } else { + try { + result.wham = { + ok: true, + windows: await fetchCodexQuota(token.token, token.accountId), + }; + } catch (error) { + result.wham = { + ok: false, + error: stringifyError(error), + windows: [], + }; + } + } + } + + if (!args.rpcOnly && !args.whamOnly) { + try { + result.aggregated = await getQuotaWindows(); + } catch (error) { + result.aggregated = { + ok: false, + error: stringifyError(error), + }; + } + } + + const rpcOk = (result.rpc as { ok?: boolean } | undefined)?.ok === true; + const whamOk = (result.wham as { ok?: boolean } | undefined)?.ok === true; + const aggregatedOk = (result.aggregated as { ok?: boolean } | undefined)?.ok === true; + const ok = rpcOk || whamOk || aggregatedOk; + + if (args.json || process.stdout.isTTY === false) { + console.log(JSON.stringify({ ok, ...result }, null, 2)); + } else { + console.log(`timestamp: ${result.timestamp}`); + console.log(`auth: ${JSON.stringify(auth)}`); + console.log(`tokenAvailable: ${token != null}`); + if (result.rpc) console.log(`rpc: ${JSON.stringify(result.rpc, null, 2)}`); + if (result.wham) console.log(`wham: ${JSON.stringify(result.wham, null, 2)}`); + if (result.aggregated) console.log(`aggregated: ${JSON.stringify(result.aggregated, null, 2)}`); + } + + if (!ok) process.exitCode = 1; +} + +await main(); diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index e1718cc1..b0c72ad5 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclipai/adapter-utils"; +import { inferOpenAiCompatibleBiller, type AdapterExecutionContext, type AdapterExecutionResult } from "@paperclipai/adapter-utils"; import { asString, asNumber, @@ -61,6 +61,12 @@ function resolveCodexBillingType(env: Record): "api" | "subscrip return hasNonEmptyEnvValue(env, "OPENAI_API_KEY") ? "api" : "subscription"; } +function resolveCodexBiller(env: Record, billingType: "api" | "subscription"): string { + const openAiCompatibleBiller = inferOpenAiCompatibleBiller(env, "openai"); + if (openAiCompatibleBiller === "openrouter") return "openrouter"; + return billingType === "subscription" ? "chatgpt" : openAiCompatibleBiller ?? "openai"; +} + async function isLikelyPaperclipRepoRoot(candidate: string): Promise { const [hasWorkspace, hasPackageJson, hasServerDir, hasAdapterUtilsDir] = await Promise.all([ pathExists(path.join(candidate, "pnpm-workspace.yaml")), @@ -315,8 +321,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise typeof entry[1] === "string", + ), + ); + const billingType = resolveCodexBillingType(effectiveEnv); + const runtimeEnv = ensurePathInEnv(effectiveEnv); await ensureCommandResolvable(command, cwd, runtimeEnv); const timeoutSec = asNumber(config.timeoutSec, 0); @@ -508,6 +519,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) return fromEnv.trim(); return path.join(os.homedir(), ".codex"); } -interface CodexAuthFile { +interface CodexLegacyAuthFile { accessToken?: string | null; accountId?: string | null; } -export async function readCodexToken(): Promise<{ token: string; accountId: string | null } | null> { +interface CodexTokenBlock { + id_token?: string | null; + access_token?: string | null; + refresh_token?: string | null; + account_id?: string | null; +} + +interface CodexModernAuthFile { + OPENAI_API_KEY?: string | null; + tokens?: CodexTokenBlock | null; + last_refresh?: string | null; +} + +export interface CodexAuthInfo { + accessToken: string; + accountId: string | null; + refreshToken: string | null; + idToken: string | null; + email: string | null; + planType: string | null; + lastRefresh: string | null; +} + +function base64UrlDecode(input: string): string | null { + try { + let normalized = input.replace(/-/g, "+").replace(/_/g, "/"); + const remainder = normalized.length % 4; + if (remainder > 0) normalized += "=".repeat(4 - remainder); + return Buffer.from(normalized, "base64").toString("utf8"); + } catch { + return null; + } +} + +function decodeJwtPayload(token: string | null | undefined): Record | null { + if (typeof token !== "string" || token.trim().length === 0) return null; + const parts = token.split("."); + if (parts.length < 2) return null; + const decoded = base64UrlDecode(parts[1] ?? ""); + if (!decoded) return null; + try { + const parsed = JSON.parse(decoded) as unknown; + return typeof parsed === "object" && parsed !== null ? parsed as Record : null; + } catch { + return null; + } +} + +function readNestedString(record: Record, pathSegments: string[]): string | null { + let current: unknown = record; + for (const segment of pathSegments) { + if (typeof current !== "object" || current === null || Array.isArray(current)) return null; + current = (current as Record)[segment]; + } + return typeof current === "string" && current.trim().length > 0 ? current.trim() : null; +} + +function parsePlanAndEmailFromToken(idToken: string | null, accessToken: string | null): { + email: string | null; + planType: string | null; +} { + const payloads = [decodeJwtPayload(idToken), decodeJwtPayload(accessToken)].filter( + (value): value is Record => value != null, + ); + for (const payload of payloads) { + const directEmail = typeof payload.email === "string" ? payload.email : null; + const authBlock = + typeof payload["https://api.openai.com/auth"] === "object" && + payload["https://api.openai.com/auth"] !== null && + !Array.isArray(payload["https://api.openai.com/auth"]) + ? payload["https://api.openai.com/auth"] as Record + : null; + const profileBlock = + typeof payload["https://api.openai.com/profile"] === "object" && + payload["https://api.openai.com/profile"] !== null && + !Array.isArray(payload["https://api.openai.com/profile"]) + ? payload["https://api.openai.com/profile"] as Record + : null; + const email = + directEmail + ?? (typeof profileBlock?.email === "string" ? profileBlock.email : null) + ?? (typeof authBlock?.chatgpt_user_email === "string" ? authBlock.chatgpt_user_email : null); + const planType = + typeof authBlock?.chatgpt_plan_type === "string" ? authBlock.chatgpt_plan_type : null; + if (email || planType) return { email: email ?? null, planType }; + } + return { email: null, planType: null }; +} + +export async function readCodexAuthInfo(): Promise { const authPath = path.join(codexHomeDir(), "auth.json"); let raw: string; try { @@ -29,18 +122,55 @@ export async function readCodexToken(): Promise<{ token: string; accountId: stri return null; } if (typeof parsed !== "object" || parsed === null) return null; - const obj = parsed as CodexAuthFile; - const token = obj.accessToken; - if (typeof token !== "string" || token.length === 0) return null; + const obj = parsed as Record; + const modern = obj as CodexModernAuthFile; + const legacy = obj as CodexLegacyAuthFile; + + const accessToken = + legacy.accessToken + ?? modern.tokens?.access_token + ?? readNestedString(obj, ["tokens", "access_token"]); + if (typeof accessToken !== "string" || accessToken.length === 0) return null; + const accountId = - typeof obj.accountId === "string" && obj.accountId.length > 0 ? obj.accountId : null; - return { token, accountId }; + legacy.accountId + ?? modern.tokens?.account_id + ?? readNestedString(obj, ["tokens", "account_id"]); + const refreshToken = + modern.tokens?.refresh_token + ?? readNestedString(obj, ["tokens", "refresh_token"]); + const idToken = + modern.tokens?.id_token + ?? readNestedString(obj, ["tokens", "id_token"]); + const { email, planType } = parsePlanAndEmailFromToken(idToken, accessToken); + + return { + accessToken, + accountId: + typeof accountId === "string" && accountId.trim().length > 0 ? accountId.trim() : null, + refreshToken: + typeof refreshToken === "string" && refreshToken.trim().length > 0 ? refreshToken.trim() : null, + idToken: + typeof idToken === "string" && idToken.trim().length > 0 ? idToken.trim() : null, + email, + planType, + lastRefresh: + typeof modern.last_refresh === "string" && modern.last_refresh.trim().length > 0 + ? modern.last_refresh.trim() + : null, + }; +} + +export async function readCodexToken(): Promise<{ token: string; accountId: string | null } | null> { + const auth = await readCodexAuthInfo(); + if (!auth) return null; + return { token: auth.accessToken, accountId: auth.accountId }; } interface WhamWindow { used_percent?: number | null; limit_window_seconds?: number | null; - reset_at?: string | null; + reset_at?: string | number | null; } interface WhamCredits { @@ -49,6 +179,7 @@ interface WhamCredits { } interface WhamUsageResponse { + plan_type?: string | null; rate_limit?: { primary_window?: WhamWindow | null; secondary_window?: WhamWindow | null; @@ -69,7 +200,6 @@ export function secondsToWindowLabel( if (hours < 6) return "5h"; if (hours <= 24) return "24h"; if (hours <= 168) return "7d"; - // for windows larger than 7d, show the actual day count rather than silently mislabelling return `${Math.round(hours / 24)}d`; } @@ -88,6 +218,11 @@ export async function fetchWithTimeout( } } +function normalizeCodexUsedPercent(rawPct: number | null | undefined): number | null { + if (rawPct == null) return null; + return Math.min(100, Math.round(rawPct < 1 ? rawPct * 100 : rawPct)); +} + export async function fetchCodexQuota( token: string, accountId: string | null, @@ -105,30 +240,28 @@ export async function fetchCodexQuota( const rateLimit = body.rate_limit; if (rateLimit?.primary_window != null) { const w = rateLimit.primary_window; - // wham used_percent is 0-100 (confirmed empirically); guard against 0-1 format just in case. - // use < 1 (not <= 1) so that 1% usage (rawPct=1) is not misclassified as 100%. - const rawPct = w.used_percent ?? null; - const usedPercent = - rawPct != null ? Math.min(100, Math.round(rawPct < 1 ? rawPct * 100 : rawPct)) : null; windows.push({ - label: secondsToWindowLabel(w.limit_window_seconds, "Primary"), - usedPercent, - resetsAt: w.reset_at ?? null, + label: "5h limit", + usedPercent: normalizeCodexUsedPercent(w.used_percent), + resetsAt: + typeof w.reset_at === "number" + ? unixSecondsToIso(w.reset_at) + : (w.reset_at ?? null), valueLabel: null, + detail: null, }); } if (rateLimit?.secondary_window != null) { const w = rateLimit.secondary_window; - // wham used_percent is 0-100 (confirmed empirically); guard against 0-1 format just in case. - // use < 1 (not <= 1) so that 1% usage (rawPct=1) is not misclassified as 100%. - const rawPct = w.used_percent ?? null; - const usedPercent = - rawPct != null ? Math.min(100, Math.round(rawPct < 1 ? rawPct * 100 : rawPct)) : null; windows.push({ - label: secondsToWindowLabel(w.limit_window_seconds, "Secondary"), - usedPercent, - resetsAt: w.reset_at ?? null, + label: "Weekly limit", + usedPercent: normalizeCodexUsedPercent(w.used_percent), + resetsAt: + typeof w.reset_at === "number" + ? unixSecondsToIso(w.reset_at) + : (w.reset_at ?? null), valueLabel: null, + detail: null, }); } if (body.credits != null && body.credits.unlimited !== true) { @@ -139,16 +272,285 @@ export async function fetchCodexQuota( usedPercent: null, resetsAt: null, valueLabel, + detail: null, }); } return windows; } -export async function getQuotaWindows(): Promise { - const auth = await readCodexToken(); - if (!auth) { - return { provider: "openai", ok: false, error: "no local codex auth token", windows: [] }; - } - const windows = await fetchCodexQuota(auth.token, auth.accountId); - return { provider: "openai", ok: true, windows }; +interface CodexRpcWindow { + usedPercent?: number | null; + windowDurationMins?: number | null; + resetsAt?: number | null; +} + +interface CodexRpcCredits { + hasCredits?: boolean | null; + unlimited?: boolean | null; + balance?: string | number | null; +} + +interface CodexRpcLimit { + limitId?: string | null; + limitName?: string | null; + primary?: CodexRpcWindow | null; + secondary?: CodexRpcWindow | null; + credits?: CodexRpcCredits | null; + planType?: string | null; +} + +interface CodexRpcRateLimitsResult { + rateLimits?: CodexRpcLimit | null; + rateLimitsByLimitId?: Record | null; +} + +interface CodexRpcAccountResult { + account?: { + type?: string | null; + email?: string | null; + planType?: string | null; + } | null; + requiresOpenaiAuth?: boolean | null; +} + +export interface CodexRpcQuotaSnapshot { + windows: QuotaWindow[]; + email: string | null; + planType: string | null; +} + +function unixSecondsToIso(value: number | null | undefined): string | null { + if (typeof value !== "number" || !Number.isFinite(value)) return null; + return new Date(value * 1000).toISOString(); +} + +function buildCodexRpcWindow(label: string, window: CodexRpcWindow | null | undefined): QuotaWindow | null { + if (!window) return null; + return { + label, + usedPercent: normalizeCodexUsedPercent(window.usedPercent), + resetsAt: unixSecondsToIso(window.resetsAt), + valueLabel: null, + detail: null, + }; +} + +function parseCreditBalance(value: string | number | null | undefined): string | null { + if (typeof value === "number" && Number.isFinite(value)) { + return `$${value.toFixed(2)} remaining`; + } + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return `$${parsed.toFixed(2)} remaining`; + } + return value.trim(); + } + return null; +} + +export function mapCodexRpcQuota(result: CodexRpcRateLimitsResult, account?: CodexRpcAccountResult | null): CodexRpcQuotaSnapshot { + const windows: QuotaWindow[] = []; + const limitOrder = ["codex"]; + const limitsById = result.rateLimitsByLimitId ?? {}; + for (const key of Object.keys(limitsById)) { + if (!limitOrder.includes(key)) limitOrder.push(key); + } + + const rootLimit = result.rateLimits ?? null; + const allLimits = new Map(); + if (rootLimit?.limitId) allLimits.set(rootLimit.limitId, rootLimit); + for (const [key, value] of Object.entries(limitsById)) { + allLimits.set(key, value); + } + if (!allLimits.has("codex") && rootLimit) allLimits.set("codex", rootLimit); + + for (const limitId of limitOrder) { + const limit = allLimits.get(limitId); + if (!limit) continue; + const prefix = + limitId === "codex" + ? "" + : `${limit.limitName ?? limitId} · `; + const primary = buildCodexRpcWindow(`${prefix}5h limit`, limit.primary); + if (primary) windows.push(primary); + const secondary = buildCodexRpcWindow(`${prefix}Weekly limit`, limit.secondary); + if (secondary) windows.push(secondary); + if (limitId === "codex" && limit.credits && limit.credits.unlimited !== true) { + windows.push({ + label: "Credits", + usedPercent: null, + resetsAt: null, + valueLabel: parseCreditBalance(limit.credits.balance) ?? "N/A", + detail: null, + }); + } + } + + return { + windows, + email: + typeof account?.account?.email === "string" && account.account.email.trim().length > 0 + ? account.account.email.trim() + : null, + planType: + typeof account?.account?.planType === "string" && account.account.planType.trim().length > 0 + ? account.account.planType.trim() + : (typeof rootLimit?.planType === "string" && rootLimit.planType.trim().length > 0 ? rootLimit.planType.trim() : null), + }; +} + +type PendingRequest = { + resolve: (value: Record) => void; + reject: (error: Error) => void; + timer: NodeJS.Timeout; +}; + +class CodexRpcClient { + private proc = spawn( + "codex", + ["-s", "read-only", "-a", "untrusted", "app-server"], + { stdio: ["pipe", "pipe", "pipe"], env: process.env }, + ); + + private nextId = 1; + private buffer = ""; + private pending = new Map(); + private stderr = ""; + + constructor() { + this.proc.stdout.setEncoding("utf8"); + this.proc.stderr.setEncoding("utf8"); + this.proc.stdout.on("data", (chunk: string) => this.onStdout(chunk)); + this.proc.stderr.on("data", (chunk: string) => { + this.stderr += chunk; + }); + this.proc.on("exit", () => { + for (const request of this.pending.values()) { + clearTimeout(request.timer); + request.reject(new Error(this.stderr.trim() || "codex app-server closed unexpectedly")); + } + this.pending.clear(); + }); + } + + private onStdout(chunk: string) { + this.buffer += chunk; + while (true) { + const newlineIndex = this.buffer.indexOf("\n"); + if (newlineIndex < 0) break; + const line = this.buffer.slice(0, newlineIndex).trim(); + this.buffer = this.buffer.slice(newlineIndex + 1); + if (!line) continue; + let parsed: Record; + try { + parsed = JSON.parse(line) as Record; + } catch { + continue; + } + const id = typeof parsed.id === "number" ? parsed.id : null; + if (id == null) continue; + const pending = this.pending.get(id); + if (!pending) continue; + this.pending.delete(id); + clearTimeout(pending.timer); + pending.resolve(parsed); + } + } + + private request(method: string, params: Record = {}, timeoutMs = 6_000): Promise> { + const id = this.nextId++; + const payload = JSON.stringify({ id, method, params }) + "\n"; + return new Promise>((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new Error(`codex app-server timed out on ${method}`)); + }, timeoutMs); + this.pending.set(id, { resolve, reject, timer }); + this.proc.stdin.write(payload); + }); + } + + private notify(method: string, params: Record = {}) { + this.proc.stdin.write(JSON.stringify({ method, params }) + "\n"); + } + + async initialize() { + await this.request("initialize", { + clientInfo: { + name: "paperclip", + version: "0.0.0", + }, + }); + this.notify("initialized", {}); + } + + async fetchRateLimits(): Promise { + const message = await this.request("account/rateLimits/read"); + return (message.result as CodexRpcRateLimitsResult | undefined) ?? {}; + } + + async fetchAccount(): Promise { + try { + const message = await this.request("account/read"); + return (message.result as CodexRpcAccountResult | undefined) ?? null; + } catch { + return null; + } + } + + async shutdown() { + this.proc.kill("SIGTERM"); + } +} + +export async function fetchCodexRpcQuota(): Promise { + const client = new CodexRpcClient(); + try { + await client.initialize(); + const [limits, account] = await Promise.all([ + client.fetchRateLimits(), + client.fetchAccount(), + ]); + return mapCodexRpcQuota(limits, account); + } finally { + await client.shutdown(); + } +} + +function formatProviderError(source: string, error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + return `${source}: ${message}`; +} + +export async function getQuotaWindows(): Promise { + const errors: string[] = []; + + try { + const rpc = await fetchCodexRpcQuota(); + if (rpc.windows.length > 0) { + return { provider: "openai", source: CODEX_USAGE_SOURCE_RPC, ok: true, windows: rpc.windows }; + } + } catch (error) { + errors.push(formatProviderError("Codex app-server", error)); + } + + const auth = await readCodexToken(); + if (auth) { + try { + const windows = await fetchCodexQuota(auth.token, auth.accountId); + return { provider: "openai", source: CODEX_USAGE_SOURCE_WHAM, ok: true, windows }; + } catch (error) { + errors.push(formatProviderError("ChatGPT WHAM usage", error)); + } + } else { + errors.push("no local codex auth token"); + } + + return { + provider: "openai", + ok: false, + error: errors.join("; "), + windows: [], + }; } diff --git a/packages/adapters/cursor-local/src/server/execute.ts b/packages/adapters/cursor-local/src/server/execute.ts index 5f369e11..088a9057 100644 --- a/packages/adapters/cursor-local/src/server/execute.ts +++ b/packages/adapters/cursor-local/src/server/execute.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclipai/adapter-utils"; +import { inferOpenAiCompatibleBiller, type AdapterExecutionContext, type AdapterExecutionResult } from "@paperclipai/adapter-utils"; import { asString, asNumber, @@ -47,6 +47,17 @@ function resolveCursorBillingType(env: Record): "api" | "subscri : "subscription"; } +function resolveCursorBiller( + env: Record, + billingType: "api" | "subscription", + provider: string | null, +): string { + const openAiCompatibleBiller = inferOpenAiCompatibleBiller(env, null); + if (openAiCompatibleBiller === "openrouter") return "openrouter"; + if (billingType === "subscription") return "cursor"; + return provider ?? "cursor"; +} + function resolveProviderFromModel(model: string): string | null { const trimmed = model.trim().toLowerCase(); if (!trimmed) return null; @@ -243,8 +254,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise typeof entry[1] === "string", + ), + ); + const billingType = resolveCursorBillingType(effectiveEnv); + const runtimeEnv = ensurePathInEnv(effectiveEnv); await ensureCommandResolvable(command, cwd, runtimeEnv); const timeoutSec = asNumber(config.timeoutSec, 0); @@ -474,6 +490,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise typeof entry[1] === "string", + ), + ); + const billingType = resolveGeminiBillingType(effectiveEnv); + const runtimeEnv = ensurePathInEnv(effectiveEnv); await ensureCommandResolvable(command, cwd, runtimeEnv); const timeoutSec = asNumber(config.timeoutSec, 0); @@ -420,6 +425,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise, provider: string | null): string { + return inferOpenAiCompatibleBiller(env, null) ?? provider ?? "unknown"; +} + function claudeSkillsHome(): string { return path.join(os.homedir(), ".claude", "skills"); } @@ -361,6 +365,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise, provider: string | null): string { + return inferOpenAiCompatibleBiller(env, null) ?? provider ?? "unknown"; +} + async function ensurePiSkillsInjected(onLog: AdapterExecutionContext["onLog"]) { const skillsEntries = await listPaperclipSkillEntries(__moduleDir); if (skillsEntries.length === 0) return; @@ -447,6 +451,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise Promise; }; +function toError(error: unknown, fallbackMessage: string): Error { + if (error instanceof Error) return error; + if (error === undefined) return new Error(fallbackMessage); + if (typeof error === "string") return new Error(`${fallbackMessage}: ${error}`); + + try { + return new Error(`${fallbackMessage}: ${JSON.stringify(error)}`); + } catch { + return new Error(`${fallbackMessage}: ${String(error)}`); + } +} + function readRunningPostmasterPid(postmasterPidFile: string): number | null { if (!existsSync(postmasterPidFile)) return null; try { @@ -51,6 +64,31 @@ function readPidFilePort(postmasterPidFile: string): number | null { } } +async function isPortInUse(port: number): Promise { + return await new Promise((resolve) => { + const server = createServer(); + server.unref(); + server.once("error", (error: NodeJS.ErrnoException) => { + resolve(error.code === "EADDRINUSE"); + }); + server.listen(port, "127.0.0.1", () => { + server.close(); + resolve(false); + }); + }); +} + +async function findAvailablePort(startPort: number): Promise { + const maxLookahead = 20; + let port = startPort; + for (let i = 0; i < maxLookahead; i += 1, port += 1) { + if (!(await isPortInUse(port))) return port; + } + throw new Error( + `Embedded PostgreSQL could not find a free port from ${startPort} to ${startPort + maxLookahead - 1}`, + ); +} + async function loadEmbeddedPostgresCtor(): Promise { const require = createRequire(import.meta.url); const resolveCandidates = [ @@ -76,6 +114,7 @@ async function ensureEmbeddedPostgresConnection( preferredPort: number, ): Promise { const EmbeddedPostgres = await loadEmbeddedPostgresCtor(); + const selectedPort = await findAvailablePort(preferredPort); const postmasterPidFile = path.resolve(dataDir, "postmaster.pid"); const runningPid = readRunningPostmasterPid(postmasterPidFile); const runningPort = readPidFilePort(postmasterPidFile); @@ -95,7 +134,7 @@ async function ensureEmbeddedPostgresConnection( databaseDir: dataDir, user: "paperclip", password: "paperclip", - port: preferredPort, + port: selectedPort, persistent: true, initdbFlags: ["--encoding=UTF8", "--locale=C"], onLog: () => {}, @@ -103,19 +142,30 @@ async function ensureEmbeddedPostgresConnection( }); if (!existsSync(path.resolve(dataDir, "PG_VERSION"))) { - await instance.initialise(); + try { + await instance.initialise(); + } catch (error) { + throw toError( + error, + `Failed to initialize embedded PostgreSQL cluster in ${dataDir} on port ${selectedPort}`, + ); + } } if (existsSync(postmasterPidFile)) { rmSync(postmasterPidFile, { force: true }); } - await instance.start(); + try { + await instance.start(); + } catch (error) { + throw toError(error, `Failed to start embedded PostgreSQL on port ${selectedPort}`); + } - const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${preferredPort}/postgres`; + const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${selectedPort}/postgres`; await ensurePostgresDatabase(adminConnectionString, "paperclip"); return { - connectionString: `postgres://paperclip:paperclip@127.0.0.1:${preferredPort}/paperclip`, - source: `embedded-postgres@${preferredPort}`, + connectionString: `postgres://paperclip:paperclip@127.0.0.1:${selectedPort}/paperclip`, + source: `embedded-postgres@${selectedPort}`, stop: async () => { await instance.stop(); }, diff --git a/packages/db/src/migration-status.ts b/packages/db/src/migration-status.ts index 3d0cc8f4..64db8c8c 100644 --- a/packages/db/src/migration-status.ts +++ b/packages/db/src/migration-status.ts @@ -3,6 +3,18 @@ import { resolveMigrationConnection } from "./migration-runtime.js"; const jsonMode = process.argv.includes("--json"); +function toError(error: unknown, context = "Migration status check failed"): Error { + if (error instanceof Error) return error; + if (error === undefined) return new Error(context); + if (typeof error === "string") return new Error(`${context}: ${error}`); + + try { + return new Error(`${context}: ${JSON.stringify(error)}`); + } catch { + return new Error(`${context}: ${String(error)}`); + } +} + async function main(): Promise { const connection = await resolveMigrationConnection(); @@ -42,4 +54,8 @@ async function main(): Promise { } } -await main(); +main().catch((error) => { + const err = toError(error, "Migration status check failed"); + process.stderr.write(`${err.stack ?? err.message}\n`); + process.exit(1); +}); diff --git a/packages/db/src/migrations/0031_zippy_magma.sql b/packages/db/src/migrations/0031_zippy_magma.sql new file mode 100644 index 00000000..c58597a9 --- /dev/null +++ b/packages/db/src/migrations/0031_zippy_magma.sql @@ -0,0 +1,51 @@ +CREATE TABLE "finance_events" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "agent_id" uuid, + "issue_id" uuid, + "project_id" uuid, + "goal_id" uuid, + "heartbeat_run_id" uuid, + "cost_event_id" uuid, + "billing_code" text, + "description" text, + "event_kind" text NOT NULL, + "direction" text DEFAULT 'debit' NOT NULL, + "biller" text NOT NULL, + "provider" text, + "execution_adapter_type" text, + "pricing_tier" text, + "region" text, + "model" text, + "quantity" integer, + "unit" text, + "amount_cents" integer NOT NULL, + "currency" text DEFAULT 'USD' NOT NULL, + "estimated" boolean DEFAULT false NOT NULL, + "external_invoice_id" text, + "metadata_json" jsonb, + "occurred_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "cost_events" ADD COLUMN "heartbeat_run_id" uuid;--> statement-breakpoint +ALTER TABLE "cost_events" ADD COLUMN "biller" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint +ALTER TABLE "cost_events" ADD COLUMN "billing_type" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint +ALTER TABLE "cost_events" ADD COLUMN "cached_input_tokens" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "finance_events" ADD CONSTRAINT "finance_events_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "finance_events" ADD CONSTRAINT "finance_events_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "finance_events" ADD CONSTRAINT "finance_events_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "finance_events" ADD CONSTRAINT "finance_events_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "finance_events" ADD CONSTRAINT "finance_events_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "finance_events" ADD CONSTRAINT "finance_events_heartbeat_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("heartbeat_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "finance_events" ADD CONSTRAINT "finance_events_cost_event_id_cost_events_id_fk" FOREIGN KEY ("cost_event_id") REFERENCES "public"."cost_events"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "finance_events_company_occurred_idx" ON "finance_events" USING btree ("company_id","occurred_at");--> statement-breakpoint +CREATE INDEX "finance_events_company_biller_occurred_idx" ON "finance_events" USING btree ("company_id","biller","occurred_at");--> statement-breakpoint +CREATE INDEX "finance_events_company_kind_occurred_idx" ON "finance_events" USING btree ("company_id","event_kind","occurred_at");--> statement-breakpoint +CREATE INDEX "finance_events_company_direction_occurred_idx" ON "finance_events" USING btree ("company_id","direction","occurred_at");--> statement-breakpoint +CREATE INDEX "finance_events_company_heartbeat_run_idx" ON "finance_events" USING btree ("company_id","heartbeat_run_id");--> statement-breakpoint +CREATE INDEX "finance_events_company_cost_event_idx" ON "finance_events" USING btree ("company_id","cost_event_id");--> statement-breakpoint +ALTER TABLE "cost_events" ADD CONSTRAINT "cost_events_heartbeat_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("heartbeat_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "cost_events_company_provider_occurred_idx" ON "cost_events" USING btree ("company_id","provider","occurred_at");--> statement-breakpoint +CREATE INDEX "cost_events_company_biller_occurred_idx" ON "cost_events" USING btree ("company_id","biller","occurred_at");--> statement-breakpoint +CREATE INDEX "cost_events_company_heartbeat_run_idx" ON "cost_events" USING btree ("company_id","heartbeat_run_id"); \ No newline at end of file diff --git a/packages/db/src/migrations/0032_pretty_doctor_octopus.sql b/packages/db/src/migrations/0032_pretty_doctor_octopus.sql new file mode 100644 index 00000000..6e06b563 --- /dev/null +++ b/packages/db/src/migrations/0032_pretty_doctor_octopus.sql @@ -0,0 +1,102 @@ +CREATE TABLE "budget_incidents" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "policy_id" uuid NOT NULL, + "scope_type" text NOT NULL, + "scope_id" uuid NOT NULL, + "metric" text NOT NULL, + "window_kind" text NOT NULL, + "window_start" timestamp with time zone NOT NULL, + "window_end" timestamp with time zone NOT NULL, + "threshold_type" text NOT NULL, + "amount_limit" integer NOT NULL, + "amount_observed" integer NOT NULL, + "status" text DEFAULT 'open' NOT NULL, + "approval_id" uuid, + "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 +); +--> statement-breakpoint +CREATE TABLE "budget_policies" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "scope_type" text NOT NULL, + "scope_id" uuid NOT NULL, + "metric" text DEFAULT 'billed_cents' NOT NULL, + "window_kind" text NOT NULL, + "amount" integer DEFAULT 0 NOT NULL, + "warn_percent" integer DEFAULT 80 NOT NULL, + "hard_stop_enabled" boolean DEFAULT true NOT NULL, + "notify_enabled" boolean DEFAULT true NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "created_by_user_id" text, + "updated_by_user_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "agents" ADD COLUMN "pause_reason" text;--> statement-breakpoint +ALTER TABLE "agents" ADD COLUMN "paused_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "projects" ADD COLUMN "pause_reason" text;--> statement-breakpoint +ALTER TABLE "projects" ADD COLUMN "paused_at" timestamp with time zone;--> statement-breakpoint +INSERT INTO "budget_policies" ( + "company_id", + "scope_type", + "scope_id", + "metric", + "window_kind", + "amount", + "warn_percent", + "hard_stop_enabled", + "notify_enabled", + "is_active" +) +SELECT + "id", + 'company', + "id", + 'billed_cents', + 'calendar_month_utc', + "budget_monthly_cents", + 80, + true, + true, + true +FROM "companies" +WHERE "budget_monthly_cents" > 0;--> statement-breakpoint +INSERT INTO "budget_policies" ( + "company_id", + "scope_type", + "scope_id", + "metric", + "window_kind", + "amount", + "warn_percent", + "hard_stop_enabled", + "notify_enabled", + "is_active" +) +SELECT + "company_id", + 'agent', + "id", + 'billed_cents', + 'calendar_month_utc', + "budget_monthly_cents", + 80, + true, + true, + true +FROM "agents" +WHERE "budget_monthly_cents" > 0;--> statement-breakpoint +ALTER TABLE "budget_incidents" ADD CONSTRAINT "budget_incidents_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "budget_incidents" ADD CONSTRAINT "budget_incidents_policy_id_budget_policies_id_fk" FOREIGN KEY ("policy_id") REFERENCES "public"."budget_policies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "budget_incidents" ADD CONSTRAINT "budget_incidents_approval_id_approvals_id_fk" FOREIGN KEY ("approval_id") REFERENCES "public"."approvals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "budget_policies" ADD CONSTRAINT "budget_policies_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "budget_incidents_company_status_idx" ON "budget_incidents" USING btree ("company_id","status");--> statement-breakpoint +CREATE INDEX "budget_incidents_company_scope_idx" ON "budget_incidents" USING btree ("company_id","scope_type","scope_id","status");--> statement-breakpoint +CREATE UNIQUE INDEX "budget_incidents_policy_window_threshold_idx" ON "budget_incidents" USING btree ("policy_id","window_start","threshold_type");--> statement-breakpoint +CREATE INDEX "budget_policies_company_scope_active_idx" ON "budget_policies" USING btree ("company_id","scope_type","scope_id","is_active");--> statement-breakpoint +CREATE INDEX "budget_policies_company_window_idx" ON "budget_policies" USING btree ("company_id","window_kind","metric");--> statement-breakpoint +CREATE UNIQUE INDEX "budget_policies_company_scope_metric_unique_idx" ON "budget_policies" USING btree ("company_id","scope_type","scope_id","metric","window_kind"); diff --git a/packages/db/src/migrations/meta/0031_snapshot.json b/packages/db/src/migrations/meta/0031_snapshot.json new file mode 100644 index 00000000..aa6b78b2 --- /dev/null +++ b/packages/db/src/migrations/meta/0031_snapshot.json @@ -0,0 +1,7242 @@ +{ + "id": "67faba5f-0106-4163-81f6-cd0d90488574", + "prevId": "ff007d90-e1a0-4df3-beab-a5be4a47273c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_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": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_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": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_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 + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "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": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_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": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "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": "'active'" + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "brand_color": { + "name": "brand_color", + "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": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "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": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_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": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_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": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_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": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_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": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "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": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "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 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "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": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_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": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_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": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_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": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_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": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "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": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "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 + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_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": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_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": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "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": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_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": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "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": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "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": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "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": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_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": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "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": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/src/migrations/meta/0032_snapshot.json b/packages/db/src/migrations/meta/0032_snapshot.json new file mode 100644 index 00000000..f26fee03 --- /dev/null +++ b/packages/db/src/migrations/meta/0032_snapshot.json @@ -0,0 +1,7733 @@ +{ + "id": "fd2770d1-d831-4d2a-989b-ad4bf92e575e", + "prevId": "67faba5f-0106-4163-81f6-cd0d90488574", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_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": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_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": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_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 + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "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": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_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": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_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": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "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": "'active'" + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "brand_color": { + "name": "brand_color", + "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": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "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": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_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": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_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": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_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": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_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": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "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": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "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 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "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": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_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": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_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": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_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": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_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": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "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": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "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 + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_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": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_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": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "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": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_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": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "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": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "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": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "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": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_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": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "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": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 696c437a..3af4debf 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -218,6 +218,20 @@ "when": 1773670925214, "tag": "0030_rich_magneto", "breakpoints": true + }, + { + "idx": 31, + "version": "7", + "when": 1773511922713, + "tag": "0031_zippy_magma", + "breakpoints": true + }, + { + "idx": 32, + "version": "7", + "when": 1773542934499, + "tag": "0032_pretty_doctor_octopus", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/db/src/schema/agents.ts b/packages/db/src/schema/agents.ts index cf0635cf..0bca7f64 100644 --- a/packages/db/src/schema/agents.ts +++ b/packages/db/src/schema/agents.ts @@ -27,6 +27,8 @@ export const agents = pgTable( runtimeConfig: jsonb("runtime_config").$type>().notNull().default({}), budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0), spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0), + pauseReason: text("pause_reason"), + pausedAt: timestamp("paused_at", { withTimezone: true }), permissions: jsonb("permissions").$type>().notNull().default({}), lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }), metadata: jsonb("metadata").$type>(), diff --git a/packages/db/src/schema/budget_incidents.ts b/packages/db/src/schema/budget_incidents.ts new file mode 100644 index 00000000..ff0564ba --- /dev/null +++ b/packages/db/src/schema/budget_incidents.ts @@ -0,0 +1,41 @@ +import { index, integer, pgTable, text, timestamp, uuid, uniqueIndex } from "drizzle-orm/pg-core"; +import { approvals } from "./approvals.js"; +import { budgetPolicies } from "./budget_policies.js"; +import { companies } from "./companies.js"; + +export const budgetIncidents = pgTable( + "budget_incidents", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + policyId: uuid("policy_id").notNull().references(() => budgetPolicies.id), + scopeType: text("scope_type").notNull(), + scopeId: uuid("scope_id").notNull(), + metric: text("metric").notNull(), + windowKind: text("window_kind").notNull(), + windowStart: timestamp("window_start", { withTimezone: true }).notNull(), + windowEnd: timestamp("window_end", { withTimezone: true }).notNull(), + thresholdType: text("threshold_type").notNull(), + amountLimit: integer("amount_limit").notNull(), + amountObserved: integer("amount_observed").notNull(), + status: text("status").notNull().default("open"), + approvalId: uuid("approval_id").references(() => approvals.id), + resolvedAt: timestamp("resolved_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyStatusIdx: index("budget_incidents_company_status_idx").on(table.companyId, table.status), + companyScopeIdx: index("budget_incidents_company_scope_idx").on( + table.companyId, + table.scopeType, + table.scopeId, + table.status, + ), + policyWindowIdx: uniqueIndex("budget_incidents_policy_window_threshold_idx").on( + table.policyId, + table.windowStart, + table.thresholdType, + ), + }), +); diff --git a/packages/db/src/schema/budget_policies.ts b/packages/db/src/schema/budget_policies.ts new file mode 100644 index 00000000..3713889b --- /dev/null +++ b/packages/db/src/schema/budget_policies.ts @@ -0,0 +1,43 @@ +import { boolean, index, integer, pgTable, text, timestamp, uuid, uniqueIndex } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; + +export const budgetPolicies = pgTable( + "budget_policies", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + scopeType: text("scope_type").notNull(), + scopeId: uuid("scope_id").notNull(), + metric: text("metric").notNull().default("billed_cents"), + windowKind: text("window_kind").notNull(), + amount: integer("amount").notNull().default(0), + warnPercent: integer("warn_percent").notNull().default(80), + hardStopEnabled: boolean("hard_stop_enabled").notNull().default(true), + notifyEnabled: boolean("notify_enabled").notNull().default(true), + isActive: boolean("is_active").notNull().default(true), + createdByUserId: text("created_by_user_id"), + updatedByUserId: text("updated_by_user_id"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyScopeActiveIdx: index("budget_policies_company_scope_active_idx").on( + table.companyId, + table.scopeType, + table.scopeId, + table.isActive, + ), + companyWindowIdx: index("budget_policies_company_window_idx").on( + table.companyId, + table.windowKind, + table.metric, + ), + companyScopeMetricUniqueIdx: uniqueIndex("budget_policies_company_scope_metric_unique_idx").on( + table.companyId, + table.scopeType, + table.scopeId, + table.metric, + table.windowKind, + ), + }), +); diff --git a/packages/db/src/schema/cost_events.ts b/packages/db/src/schema/cost_events.ts index 709dbb14..f8a36847 100644 --- a/packages/db/src/schema/cost_events.ts +++ b/packages/db/src/schema/cost_events.ts @@ -4,6 +4,7 @@ import { agents } from "./agents.js"; import { issues } from "./issues.js"; import { projects } from "./projects.js"; import { goals } from "./goals.js"; +import { heartbeatRuns } from "./heartbeat_runs.js"; export const costEvents = pgTable( "cost_events", @@ -14,10 +15,14 @@ export const costEvents = pgTable( issueId: uuid("issue_id").references(() => issues.id), projectId: uuid("project_id").references(() => projects.id), goalId: uuid("goal_id").references(() => goals.id), + heartbeatRunId: uuid("heartbeat_run_id").references(() => heartbeatRuns.id), billingCode: text("billing_code"), provider: text("provider").notNull(), + biller: text("biller").notNull().default("unknown"), + billingType: text("billing_type").notNull().default("unknown"), model: text("model").notNull(), inputTokens: integer("input_tokens").notNull().default(0), + cachedInputTokens: integer("cached_input_tokens").notNull().default(0), outputTokens: integer("output_tokens").notNull().default(0), costCents: integer("cost_cents").notNull(), occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(), @@ -30,5 +35,19 @@ export const costEvents = pgTable( table.agentId, table.occurredAt, ), + companyProviderOccurredIdx: index("cost_events_company_provider_occurred_idx").on( + table.companyId, + table.provider, + table.occurredAt, + ), + companyBillerOccurredIdx: index("cost_events_company_biller_occurred_idx").on( + table.companyId, + table.biller, + table.occurredAt, + ), + companyHeartbeatRunIdx: index("cost_events_company_heartbeat_run_idx").on( + table.companyId, + table.heartbeatRunId, + ), }), ); diff --git a/packages/db/src/schema/finance_events.ts b/packages/db/src/schema/finance_events.ts new file mode 100644 index 00000000..2d2703da --- /dev/null +++ b/packages/db/src/schema/finance_events.ts @@ -0,0 +1,67 @@ +import { pgTable, uuid, text, timestamp, integer, index, boolean, jsonb } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { agents } from "./agents.js"; +import { issues } from "./issues.js"; +import { projects } from "./projects.js"; +import { goals } from "./goals.js"; +import { heartbeatRuns } from "./heartbeat_runs.js"; +import { costEvents } from "./cost_events.js"; + +export const financeEvents = pgTable( + "finance_events", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + agentId: uuid("agent_id").references(() => agents.id), + issueId: uuid("issue_id").references(() => issues.id), + projectId: uuid("project_id").references(() => projects.id), + goalId: uuid("goal_id").references(() => goals.id), + heartbeatRunId: uuid("heartbeat_run_id").references(() => heartbeatRuns.id), + costEventId: uuid("cost_event_id").references(() => costEvents.id), + billingCode: text("billing_code"), + description: text("description"), + eventKind: text("event_kind").notNull(), + direction: text("direction").notNull().default("debit"), + biller: text("biller").notNull(), + provider: text("provider"), + executionAdapterType: text("execution_adapter_type"), + pricingTier: text("pricing_tier"), + region: text("region"), + model: text("model"), + quantity: integer("quantity"), + unit: text("unit"), + amountCents: integer("amount_cents").notNull(), + currency: text("currency").notNull().default("USD"), + estimated: boolean("estimated").notNull().default(false), + externalInvoiceId: text("external_invoice_id"), + metadataJson: jsonb("metadata_json").$type | null>(), + occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyOccurredIdx: index("finance_events_company_occurred_idx").on(table.companyId, table.occurredAt), + companyBillerOccurredIdx: index("finance_events_company_biller_occurred_idx").on( + table.companyId, + table.biller, + table.occurredAt, + ), + companyKindOccurredIdx: index("finance_events_company_kind_occurred_idx").on( + table.companyId, + table.eventKind, + table.occurredAt, + ), + companyDirectionOccurredIdx: index("finance_events_company_direction_occurred_idx").on( + table.companyId, + table.direction, + table.occurredAt, + ), + companyHeartbeatRunIdx: index("finance_events_company_heartbeat_run_idx").on( + table.companyId, + table.heartbeatRunId, + ), + companyCostEventIdx: index("finance_events_company_cost_event_idx").on( + table.companyId, + table.costEventId, + ), + }), +); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 422d7cdd..8e9180ba 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -7,6 +7,8 @@ export { companyMemberships } from "./company_memberships.js"; export { principalPermissionGrants } from "./principal_permission_grants.js"; export { invites } from "./invites.js"; export { joinRequests } from "./join_requests.js"; +export { budgetPolicies } from "./budget_policies.js"; +export { budgetIncidents } from "./budget_incidents.js"; export { agentConfigRevisions } from "./agent_config_revisions.js"; export { agentApiKeys } from "./agent_api_keys.js"; export { agentRuntimeState } from "./agent_runtime_state.js"; @@ -31,6 +33,7 @@ export { issueDocuments } from "./issue_documents.js"; export { heartbeatRuns } from "./heartbeat_runs.js"; export { heartbeatRunEvents } from "./heartbeat_run_events.js"; export { costEvents } from "./cost_events.js"; +export { financeEvents } from "./finance_events.js"; export { approvals } from "./approvals.js"; export { approvalComments } from "./approval_comments.js"; export { activityLog } from "./activity_log.js"; diff --git a/packages/db/src/schema/projects.ts b/packages/db/src/schema/projects.ts index 46d368e3..50520a3b 100644 --- a/packages/db/src/schema/projects.ts +++ b/packages/db/src/schema/projects.ts @@ -15,6 +15,8 @@ export const projects = pgTable( leadAgentId: uuid("lead_agent_id").references(() => agents.id), targetDate: date("target_date"), color: text("color"), + pauseReason: text("pause_reason"), + pausedAt: timestamp("paused_at", { withTimezone: true }), executionWorkspacePolicy: jsonb("execution_workspace_policy").$type>(), archivedAt: timestamp("archived_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index cb3986f9..7368bfde 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -137,6 +137,9 @@ export const PROJECT_STATUSES = [ ] as const; export type ProjectStatus = (typeof PROJECT_STATUSES)[number]; +export const PAUSE_REASONS = ["manual", "budget", "system"] as const; +export type PauseReason = (typeof PAUSE_REASONS)[number]; + export const PROJECT_COLORS = [ "#6366f1", // indigo "#8b5cf6", // violet @@ -150,7 +153,7 @@ export const PROJECT_COLORS = [ "#3b82f6", // blue ] as const; -export const APPROVAL_TYPES = ["hire_agent", "approve_ceo_strategy"] as const; +export const APPROVAL_TYPES = ["hire_agent", "approve_ceo_strategy", "budget_override_required"] as const; export type ApprovalType = (typeof APPROVAL_TYPES)[number]; export const APPROVAL_STATUSES = [ @@ -173,6 +176,73 @@ export type SecretProvider = (typeof SECRET_PROVIDERS)[number]; export const STORAGE_PROVIDERS = ["local_disk", "s3"] as const; export type StorageProvider = (typeof STORAGE_PROVIDERS)[number]; +export const BILLING_TYPES = [ + "metered_api", + "subscription_included", + "subscription_overage", + "credits", + "fixed", + "unknown", +] as const; +export type BillingType = (typeof BILLING_TYPES)[number]; + +export const FINANCE_EVENT_KINDS = [ + "inference_charge", + "platform_fee", + "credit_purchase", + "credit_refund", + "credit_expiry", + "byok_fee", + "gateway_overhead", + "log_storage_charge", + "logpush_charge", + "provisioned_capacity_charge", + "training_charge", + "custom_model_import_charge", + "custom_model_storage_charge", + "manual_adjustment", +] as const; +export type FinanceEventKind = (typeof FINANCE_EVENT_KINDS)[number]; + +export const FINANCE_DIRECTIONS = ["debit", "credit"] as const; +export type FinanceDirection = (typeof FINANCE_DIRECTIONS)[number]; + +export const FINANCE_UNITS = [ + "input_token", + "output_token", + "cached_input_token", + "request", + "credit_usd", + "credit_unit", + "model_unit_minute", + "model_unit_hour", + "gb_month", + "train_token", + "unknown", +] as const; +export type FinanceUnit = (typeof FINANCE_UNITS)[number]; + +export const BUDGET_SCOPE_TYPES = ["company", "agent", "project"] as const; +export type BudgetScopeType = (typeof BUDGET_SCOPE_TYPES)[number]; + +export const BUDGET_METRICS = ["billed_cents"] as const; +export type BudgetMetric = (typeof BUDGET_METRICS)[number]; + +export const BUDGET_WINDOW_KINDS = ["calendar_month_utc", "lifetime"] as const; +export type BudgetWindowKind = (typeof BUDGET_WINDOW_KINDS)[number]; + +export const BUDGET_THRESHOLD_TYPES = ["soft", "hard"] as const; +export type BudgetThresholdType = (typeof BUDGET_THRESHOLD_TYPES)[number]; + +export const BUDGET_INCIDENT_STATUSES = ["open", "resolved", "dismissed"] as const; +export type BudgetIncidentStatus = (typeof BUDGET_INCIDENT_STATUSES)[number]; + +export const BUDGET_INCIDENT_RESOLUTION_ACTIONS = [ + "keep_paused", + "raise_budget_and_resume", +] as const; +export type BudgetIncidentResolutionAction = (typeof BUDGET_INCIDENT_RESOLUTION_ACTIONS)[number]; + export const HEARTBEAT_INVOCATION_SOURCES = [ "timer", "assignment", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index a5015862..9c3f2e8f 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -13,11 +13,22 @@ export { GOAL_LEVELS, GOAL_STATUSES, PROJECT_STATUSES, + PAUSE_REASONS, PROJECT_COLORS, APPROVAL_TYPES, APPROVAL_STATUSES, SECRET_PROVIDERS, STORAGE_PROVIDERS, + BILLING_TYPES, + FINANCE_EVENT_KINDS, + FINANCE_DIRECTIONS, + FINANCE_UNITS, + BUDGET_SCOPE_TYPES, + BUDGET_METRICS, + BUDGET_WINDOW_KINDS, + BUDGET_THRESHOLD_TYPES, + BUDGET_INCIDENT_STATUSES, + BUDGET_INCIDENT_RESOLUTION_ACTIONS, HEARTBEAT_INVOCATION_SOURCES, HEARTBEAT_RUN_STATUSES, WAKEUP_TRIGGER_DETAILS, @@ -61,10 +72,21 @@ export { type GoalLevel, type GoalStatus, type ProjectStatus, + type PauseReason, type ApprovalType, type ApprovalStatus, type SecretProvider, type StorageProvider, + type BillingType, + type FinanceEventKind, + type FinanceDirection, + type FinanceUnit, + type BudgetScopeType, + type BudgetMetric, + type BudgetWindowKind, + type BudgetThresholdType, + type BudgetIncidentStatus, + type BudgetIncidentResolutionAction, type HeartbeatInvocationSource, type HeartbeatRunStatus, type WakeupTriggerDetail, @@ -129,13 +151,24 @@ export type { Goal, Approval, ApprovalComment, + BudgetPolicy, + BudgetPolicySummary, + BudgetIncident, + BudgetOverview, + BudgetPolicyUpsertInput, + BudgetIncidentResolutionInput, CostEvent, CostSummary, CostByAgent, CostByProviderModel, + CostByBiller, CostByAgentModel, CostWindowSpendRow, CostByProject, + FinanceEvent, + FinanceSummary, + FinanceByBiller, + FinanceByKind, HeartbeatRun, HeartbeatRunEvent, AgentRuntimeState, @@ -253,11 +286,15 @@ export { type CreateGoal, type UpdateGoal, createApprovalSchema, + upsertBudgetPolicySchema, + resolveBudgetIncidentSchema, resolveApprovalSchema, requestApprovalRevisionSchema, resubmitApprovalSchema, addApprovalCommentSchema, type CreateApproval, + type UpsertBudgetPolicy, + type ResolveBudgetIncident, type ResolveApproval, type RequestApprovalRevision, type ResubmitApproval, @@ -273,6 +310,7 @@ export { type RotateSecret, type UpdateSecret, createCostEventSchema, + createFinanceEventSchema, updateBudgetSchema, createAssetImageMetadataSchema, createCompanyInviteSchema, @@ -283,6 +321,7 @@ export { updateMemberPermissionsSchema, updateUserCompanyAccessSchema, type CreateCostEvent, + type CreateFinanceEvent, type UpdateBudget, type CreateAssetImageMetadata, type CreateCompanyInvite, diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts index 04a64a6d..dd1ae45f 100644 --- a/packages/shared/src/types/agent.ts +++ b/packages/shared/src/types/agent.ts @@ -1,5 +1,6 @@ import type { AgentAdapterType, + PauseReason, AgentRole, AgentStatus, } from "../constants.js"; @@ -24,6 +25,8 @@ export interface Agent { runtimeConfig: Record; budgetMonthlyCents: number; spentMonthlyCents: number; + pauseReason: PauseReason | null; + pausedAt: Date | null; permissions: AgentPermissions; lastHeartbeatAt: Date | null; metadata: Record | null; diff --git a/packages/shared/src/types/budget.ts b/packages/shared/src/types/budget.ts new file mode 100644 index 00000000..6907796a --- /dev/null +++ b/packages/shared/src/types/budget.ts @@ -0,0 +1,99 @@ +import type { + BudgetIncidentResolutionAction, + BudgetIncidentStatus, + BudgetMetric, + BudgetScopeType, + BudgetThresholdType, + BudgetWindowKind, + PauseReason, +} from "../constants.js"; + +export interface BudgetPolicy { + id: string; + companyId: string; + scopeType: BudgetScopeType; + scopeId: string; + metric: BudgetMetric; + windowKind: BudgetWindowKind; + amount: number; + warnPercent: number; + hardStopEnabled: boolean; + notifyEnabled: boolean; + isActive: boolean; + createdByUserId: string | null; + updatedByUserId: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface BudgetPolicySummary { + policyId: string; + companyId: string; + scopeType: BudgetScopeType; + scopeId: string; + scopeName: string; + metric: BudgetMetric; + windowKind: BudgetWindowKind; + amount: number; + observedAmount: number; + remainingAmount: number; + utilizationPercent: number; + warnPercent: number; + hardStopEnabled: boolean; + notifyEnabled: boolean; + isActive: boolean; + status: "ok" | "warning" | "hard_stop"; + paused: boolean; + pauseReason: PauseReason | null; + windowStart: Date; + windowEnd: Date; +} + +export interface BudgetIncident { + id: string; + companyId: string; + policyId: string; + scopeType: BudgetScopeType; + scopeId: string; + scopeName: string; + metric: BudgetMetric; + windowKind: BudgetWindowKind; + windowStart: Date; + windowEnd: Date; + thresholdType: BudgetThresholdType; + amountLimit: number; + amountObserved: number; + status: BudgetIncidentStatus; + approvalId: string | null; + approvalStatus: string | null; + resolvedAt: Date | null; + createdAt: Date; + updatedAt: Date; +} + +export interface BudgetOverview { + companyId: string; + policies: BudgetPolicySummary[]; + activeIncidents: BudgetIncident[]; + pausedAgentCount: number; + pausedProjectCount: number; + pendingApprovalCount: number; +} + +export interface BudgetPolicyUpsertInput { + scopeType: BudgetScopeType; + scopeId: string; + metric?: BudgetMetric; + windowKind?: BudgetWindowKind; + amount: number; + warnPercent?: number; + hardStopEnabled?: boolean; + notifyEnabled?: boolean; + isActive?: boolean; +} + +export interface BudgetIncidentResolutionInput { + action: BudgetIncidentResolutionAction; + amount?: number; + decisionNote?: string | null; +} diff --git a/packages/shared/src/types/cost.ts b/packages/shared/src/types/cost.ts index af2ba0e1..8a77a8f2 100644 --- a/packages/shared/src/types/cost.ts +++ b/packages/shared/src/types/cost.ts @@ -1,3 +1,5 @@ +import type { BillingType } from "../constants.js"; + export interface CostEvent { id: string; companyId: string; @@ -5,10 +7,14 @@ export interface CostEvent { issueId: string | null; projectId: string | null; goalId: string | null; + heartbeatRunId: string | null; billingCode: string | null; provider: string; + biller: string; + billingType: BillingType; model: string; inputTokens: number; + cachedInputTokens: number; outputTokens: number; costCents: number; occurredAt: Date; @@ -28,45 +34,71 @@ export interface CostByAgent { agentStatus: string | null; costCents: number; inputTokens: number; + cachedInputTokens: number; outputTokens: number; apiRunCount: number; subscriptionRunCount: number; + subscriptionCachedInputTokens: number; subscriptionInputTokens: number; subscriptionOutputTokens: number; } export interface CostByProviderModel { provider: string; + biller: string; + billingType: BillingType; model: string; costCents: number; inputTokens: number; + cachedInputTokens: number; outputTokens: number; apiRunCount: number; subscriptionRunCount: number; + subscriptionCachedInputTokens: number; subscriptionInputTokens: number; subscriptionOutputTokens: number; } +export interface CostByBiller { + biller: string; + costCents: number; + inputTokens: number; + cachedInputTokens: number; + outputTokens: number; + apiRunCount: number; + subscriptionRunCount: number; + subscriptionCachedInputTokens: number; + subscriptionInputTokens: number; + subscriptionOutputTokens: number; + providerCount: number; + modelCount: number; +} + /** per-agent breakdown by provider + model, for identifying token-hungry agents */ export interface CostByAgentModel { agentId: string; agentName: string | null; provider: string; + biller: string; + billingType: BillingType; model: string; costCents: number; inputTokens: number; + cachedInputTokens: number; outputTokens: number; } /** spend per provider for a fixed rolling time window */ export interface CostWindowSpendRow { provider: string; + biller: string; /** duration label, e.g. "5h", "24h", "7d" */ window: string; /** rolling window duration in hours */ windowHours: number; costCents: number; inputTokens: number; + cachedInputTokens: number; outputTokens: number; } @@ -76,5 +108,6 @@ export interface CostByProject { projectName: string | null; costCents: number; inputTokens: number; + cachedInputTokens: number; outputTokens: number; } diff --git a/packages/shared/src/types/dashboard.ts b/packages/shared/src/types/dashboard.ts index 8350589d..0127a4fd 100644 --- a/packages/shared/src/types/dashboard.ts +++ b/packages/shared/src/types/dashboard.ts @@ -18,4 +18,10 @@ export interface DashboardSummary { monthUtilizationPercent: number; }; pendingApprovals: number; + budgets: { + activeIncidents: number; + pendingApprovals: number; + pausedAgents: number; + pausedProjects: number; + }; } diff --git a/packages/shared/src/types/finance.ts b/packages/shared/src/types/finance.ts new file mode 100644 index 00000000..75a52084 --- /dev/null +++ b/packages/shared/src/types/finance.ts @@ -0,0 +1,60 @@ +import type { AgentAdapterType, FinanceDirection, FinanceEventKind, FinanceUnit } from "../constants.js"; + +export interface FinanceEvent { + id: string; + companyId: string; + agentId: string | null; + issueId: string | null; + projectId: string | null; + goalId: string | null; + heartbeatRunId: string | null; + costEventId: string | null; + billingCode: string | null; + description: string | null; + eventKind: FinanceEventKind; + direction: FinanceDirection; + biller: string; + provider: string | null; + executionAdapterType: AgentAdapterType | null; + pricingTier: string | null; + region: string | null; + model: string | null; + quantity: number | null; + unit: FinanceUnit | null; + amountCents: number; + currency: string; + estimated: boolean; + externalInvoiceId: string | null; + metadataJson: Record | null; + occurredAt: Date; + createdAt: Date; +} + +export interface FinanceSummary { + companyId: string; + debitCents: number; + creditCents: number; + netCents: number; + estimatedDebitCents: number; + eventCount: number; +} + +export interface FinanceByBiller { + biller: string; + debitCents: number; + creditCents: number; + netCents: number; + estimatedDebitCents: number; + eventCount: number; + kindCount: number; +} + +export interface FinanceByKind { + eventKind: FinanceEventKind; + debitCents: number; + creditCents: number; + netCents: number; + estimatedDebitCents: number; + eventCount: number; + billerCount: number; +} diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 135c0d14..ce8050e4 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -36,6 +36,14 @@ export type { } from "./issue.js"; export type { Goal } from "./goal.js"; export type { Approval, ApprovalComment } from "./approval.js"; +export type { + BudgetPolicy, + BudgetPolicySummary, + BudgetIncident, + BudgetOverview, + BudgetPolicyUpsertInput, + BudgetIncidentResolutionInput, +} from "./budget.js"; export type { SecretProvider, SecretVersionSelector, @@ -46,7 +54,8 @@ export type { CompanySecret, SecretProviderDescriptor, } from "./secrets.js"; -export type { CostEvent, CostSummary, CostByAgent, CostByProviderModel, CostByAgentModel, CostWindowSpendRow, CostByProject } from "./cost.js"; +export type { CostEvent, CostSummary, CostByAgent, CostByProviderModel, CostByBiller, CostByAgentModel, CostWindowSpendRow, CostByProject } from "./cost.js"; +export type { FinanceEvent, FinanceSummary, FinanceByBiller, FinanceByKind } from "./finance.js"; export type { HeartbeatRun, HeartbeatRunEvent, diff --git a/packages/shared/src/types/project.ts b/packages/shared/src/types/project.ts index e9981ff7..596908b9 100644 --- a/packages/shared/src/types/project.ts +++ b/packages/shared/src/types/project.ts @@ -1,4 +1,4 @@ -import type { ProjectStatus } from "../constants.js"; +import type { PauseReason, ProjectStatus } from "../constants.js"; import type { ProjectExecutionWorkspacePolicy, WorkspaceRuntimeService } from "./workspace-runtime.js"; export interface ProjectGoalRef { @@ -35,6 +35,8 @@ export interface Project { leadAgentId: string | null; targetDate: string | null; color: string | null; + pauseReason: PauseReason | null; + pausedAt: Date | null; executionWorkspacePolicy: ProjectExecutionWorkspacePolicy | null; workspaces: ProjectWorkspace[]; primaryWorkspace: ProjectWorkspace | null; diff --git a/packages/shared/src/types/quota.ts b/packages/shared/src/types/quota.ts index f5e5a391..56b68bb5 100644 --- a/packages/shared/src/types/quota.ts +++ b/packages/shared/src/types/quota.ts @@ -8,12 +8,16 @@ export interface QuotaWindow { resetsAt: string | null; /** free-form value label for credit-style windows, e.g. "$4.20 remaining" */ valueLabel: string | null; + /** optional supporting text, e.g. reset details or provider-specific notes */ + detail?: string | null; } /** result for one provider from the quota-windows endpoint */ export interface ProviderQuotaResult { /** provider slug, e.g. "anthropic", "openai" */ provider: string; + /** source label when the provider reports where the quota data came from */ + source?: string | null; /** true when the fetch succeeded and windows is populated */ ok: boolean; /** error message when ok is false */ diff --git a/packages/shared/src/validators/budget.ts b/packages/shared/src/validators/budget.ts new file mode 100644 index 00000000..abae5a90 --- /dev/null +++ b/packages/shared/src/validators/budget.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; +import { + BUDGET_INCIDENT_RESOLUTION_ACTIONS, + BUDGET_METRICS, + BUDGET_SCOPE_TYPES, + BUDGET_WINDOW_KINDS, +} from "../constants.js"; + +export const upsertBudgetPolicySchema = z.object({ + scopeType: z.enum(BUDGET_SCOPE_TYPES), + scopeId: z.string().uuid(), + metric: z.enum(BUDGET_METRICS).optional().default("billed_cents"), + windowKind: z.enum(BUDGET_WINDOW_KINDS).optional().default("calendar_month_utc"), + amount: z.number().int().nonnegative(), + warnPercent: z.number().int().min(1).max(99).optional().default(80), + hardStopEnabled: z.boolean().optional().default(true), + notifyEnabled: z.boolean().optional().default(true), + isActive: z.boolean().optional().default(true), +}); + +export type UpsertBudgetPolicy = z.infer; + +export const resolveBudgetIncidentSchema = z.object({ + action: z.enum(BUDGET_INCIDENT_RESOLUTION_ACTIONS), + amount: z.number().int().nonnegative().optional(), + decisionNote: z.string().optional().nullable(), +}).superRefine((value, ctx) => { + if (value.action === "raise_budget_and_resume" && typeof value.amount !== "number") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "amount is required when raising a budget", + path: ["amount"], + }); + } +}); + +export type ResolveBudgetIncident = z.infer; diff --git a/packages/shared/src/validators/cost.ts b/packages/shared/src/validators/cost.ts index 5509e25b..ed22d7eb 100644 --- a/packages/shared/src/validators/cost.ts +++ b/packages/shared/src/validators/cost.ts @@ -1,18 +1,26 @@ import { z } from "zod"; +import { BILLING_TYPES } from "../constants.js"; export const createCostEventSchema = z.object({ agentId: z.string().uuid(), issueId: z.string().uuid().optional().nullable(), projectId: z.string().uuid().optional().nullable(), goalId: z.string().uuid().optional().nullable(), + heartbeatRunId: z.string().uuid().optional().nullable(), billingCode: z.string().optional().nullable(), provider: z.string().min(1), + biller: z.string().min(1).optional(), + billingType: z.enum(BILLING_TYPES).optional().default("unknown"), model: z.string().min(1), inputTokens: z.number().int().nonnegative().optional().default(0), + cachedInputTokens: z.number().int().nonnegative().optional().default(0), outputTokens: z.number().int().nonnegative().optional().default(0), costCents: z.number().int().nonnegative(), occurredAt: z.string().datetime(), -}); +}).transform((value) => ({ + ...value, + biller: value.biller ?? value.provider, +})); export type CreateCostEvent = z.infer; diff --git a/packages/shared/src/validators/finance.ts b/packages/shared/src/validators/finance.ts new file mode 100644 index 00000000..1f8bd99a --- /dev/null +++ b/packages/shared/src/validators/finance.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; +import { AGENT_ADAPTER_TYPES, FINANCE_DIRECTIONS, FINANCE_EVENT_KINDS, FINANCE_UNITS } from "../constants.js"; + +export const createFinanceEventSchema = z.object({ + agentId: z.string().uuid().optional().nullable(), + issueId: z.string().uuid().optional().nullable(), + projectId: z.string().uuid().optional().nullable(), + goalId: z.string().uuid().optional().nullable(), + heartbeatRunId: z.string().uuid().optional().nullable(), + costEventId: z.string().uuid().optional().nullable(), + billingCode: z.string().optional().nullable(), + description: z.string().max(500).optional().nullable(), + eventKind: z.enum(FINANCE_EVENT_KINDS), + direction: z.enum(FINANCE_DIRECTIONS).optional().default("debit"), + biller: z.string().min(1), + provider: z.string().min(1).optional().nullable(), + executionAdapterType: z.enum(AGENT_ADAPTER_TYPES).optional().nullable(), + pricingTier: z.string().min(1).optional().nullable(), + region: z.string().min(1).optional().nullable(), + model: z.string().min(1).optional().nullable(), + quantity: z.number().int().nonnegative().optional().nullable(), + unit: z.enum(FINANCE_UNITS).optional().nullable(), + amountCents: z.number().int().nonnegative(), + currency: z.string().length(3).optional().default("USD"), + estimated: z.boolean().optional().default(false), + externalInvoiceId: z.string().optional().nullable(), + metadataJson: z.record(z.string(), z.unknown()).optional().nullable(), + occurredAt: z.string().datetime(), +}).transform((value) => ({ + ...value, + currency: value.currency.toUpperCase(), +})); + +export type CreateFinanceEvent = z.infer; diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index f3462187..18887a2e 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -1,3 +1,10 @@ +export { + upsertBudgetPolicySchema, + resolveBudgetIncidentSchema, + type UpsertBudgetPolicy, + type ResolveBudgetIncident, +} from "./budget.js"; + export { createCompanySchema, updateCompanySchema, @@ -121,6 +128,11 @@ export { type UpdateBudget, } from "./cost.js"; +export { + createFinanceEventSchema, + type CreateFinanceEvent, +} from "./finance.js"; + export { createAssetImageMetadataSchema, type CreateAssetImageMetadata, diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index 171f72f0..558cc6cb 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -46,6 +46,30 @@ if (tailscaleAuth) { const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; +function toError(error, context = "Dev runner command failed") { + if (error instanceof Error) return error; + if (error === undefined) return new Error(context); + if (typeof error === "string") return new Error(`${context}: ${error}`); + + try { + return new Error(`${context}: ${JSON.stringify(error)}`); + } catch { + return new Error(`${context}: ${String(error)}`); + } +} + +process.on("uncaughtException", (error) => { + const err = toError(error, "Uncaught exception in dev runner"); + process.stderr.write(`${err.stack ?? err.message}\n`); + process.exit(1); +}); + +process.on("unhandledRejection", (reason) => { + const err = toError(reason, "Unhandled promise rejection in dev runner"); + process.stderr.write(`${err.stack ?? err.message}\n`); + process.exit(1); +}); + function formatPendingMigrationSummary(migrations) { if (migrations.length === 0) return "none"; return migrations.length > 3 @@ -96,7 +120,11 @@ async function maybePreflightMigrations() { { env }, ); if (status.code !== 0) { - process.stderr.write(status.stderr || status.stdout); + process.stderr.write( + status.stderr || + status.stdout || + `[paperclip] Command failed with code ${status.code}: pnpm --filter @paperclipai/db exec tsx src/migration-status.ts --json\n`, + ); process.exit(status.code); } @@ -104,8 +132,12 @@ async function maybePreflightMigrations() { try { payload = JSON.parse(status.stdout.trim()); } catch (error) { - process.stderr.write(status.stderr || status.stdout); - throw error; + process.stderr.write( + status.stderr || + status.stdout || + "[paperclip] migration-status returned invalid JSON payload\n", + ); + throw toError(error, "Unable to parse migration-status JSON output"); } if (payload.status !== "needsMigrations" || payload.pendingMigrations.length === 0) { diff --git a/server/src/__tests__/companies-route-path-guard.test.ts b/server/src/__tests__/companies-route-path-guard.test.ts index ede6d024..a06a826e 100644 --- a/server/src/__tests__/companies-route-path-guard.test.ts +++ b/server/src/__tests__/companies-route-path-guard.test.ts @@ -22,6 +22,9 @@ vi.mock("../services/index.js", () => ({ canUser: vi.fn(), ensureMembership: vi.fn(), }), + budgetService: () => ({ + upsertPolicy: vi.fn(), + }), logActivity: vi.fn(), })); diff --git a/server/src/__tests__/costs-service.test.ts b/server/src/__tests__/costs-service.test.ts index ed0cf480..a4ec2278 100644 --- a/server/src/__tests__/costs-service.test.ts +++ b/server/src/__tests__/costs-service.test.ts @@ -1,14 +1,9 @@ import express from "express"; import request from "supertest"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { costRoutes } from "../routes/costs.js"; import { errorHandler } from "../middleware/index.js"; -// --------------------------------------------------------------------------- -// parseDateRange — tested via the route handler since it's a private function -// --------------------------------------------------------------------------- - -// Minimal db stub — just enough for costService() not to throw at construction function makeDb(overrides: Record = {}) { const selectChain = { from: vi.fn().mockReturnThis(), @@ -17,9 +12,10 @@ function makeDb(overrides: Record = {}) { innerJoin: vi.fn().mockReturnThis(), groupBy: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), + limit: vi.fn().mockReturnThis(), then: vi.fn().mockResolvedValue([]), }; - // Make it thenable so Drizzle query chains resolve to [] + const thenableChain = Object.assign(Promise.resolve([]), selectChain); return { @@ -43,17 +39,40 @@ const mockAgentService = vi.hoisted(() => ({ })); const mockLogActivity = vi.hoisted(() => vi.fn()); const mockFetchAllQuotaWindows = vi.hoisted(() => vi.fn()); +const mockCostService = vi.hoisted(() => ({ + createEvent: vi.fn(), + summary: vi.fn().mockResolvedValue({ spendCents: 0 }), + byAgent: vi.fn().mockResolvedValue([]), + byAgentModel: vi.fn().mockResolvedValue([]), + byProvider: vi.fn().mockResolvedValue([]), + byBiller: vi.fn().mockResolvedValue([]), + windowSpend: vi.fn().mockResolvedValue([]), + byProject: vi.fn().mockResolvedValue([]), +})); +const mockFinanceService = vi.hoisted(() => ({ + createEvent: vi.fn(), + summary: vi.fn().mockResolvedValue({ debitCents: 0, creditCents: 0, netCents: 0, estimatedDebitCents: 0, eventCount: 0 }), + byBiller: vi.fn().mockResolvedValue([]), + byKind: vi.fn().mockResolvedValue([]), + list: vi.fn().mockResolvedValue([]), +})); +const mockBudgetService = vi.hoisted(() => ({ + overview: vi.fn().mockResolvedValue({ + companyId: "company-1", + policies: [], + activeIncidents: [], + pausedAgentCount: 0, + pausedProjectCount: 0, + pendingApprovalCount: 0, + }), + upsertPolicy: vi.fn(), + resolveIncident: vi.fn(), +})); vi.mock("../services/index.js", () => ({ - costService: () => ({ - createEvent: vi.fn(), - summary: vi.fn().mockResolvedValue({ spendCents: 0 }), - byAgent: vi.fn().mockResolvedValue([]), - byAgentModel: vi.fn().mockResolvedValue([]), - byProvider: vi.fn().mockResolvedValue([]), - windowSpend: vi.fn().mockResolvedValue([]), - byProject: vi.fn().mockResolvedValue([]), - }), + budgetService: () => mockBudgetService, + costService: () => mockCostService, + financeService: () => mockFinanceService, companyService: () => mockCompanyService, agentService: () => mockAgentService, logActivity: mockLogActivity, @@ -75,8 +94,12 @@ function createApp() { return app; } -describe("parseDateRange — date validation via route", () => { - it("accepts valid ISO date strings and passes them to the service", async () => { +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("cost routes", () => { + it("accepts valid ISO date strings and passes them to cost summary routes", async () => { const app = createApp(); const res = await request(app) .get("/api/companies/company-1/costs/summary") @@ -102,138 +125,30 @@ describe("parseDateRange — date validation via route", () => { expect(res.body.error).toMatch(/invalid 'to' date/i); }); - it("treats missing 'from' and 'to' as no range (passes undefined to service)", async () => { + it("returns finance summary rows for valid requests", async () => { const app = createApp(); - const res = await request(app).get("/api/companies/company-1/costs/summary"); + const res = await request(app) + .get("/api/companies/company-1/costs/finance-summary") + .query({ from: "2026-02-01T00:00:00.000Z", to: "2026-02-28T23:59:59.999Z" }); expect(res.status).toBe(200); - }); -}); - -// --------------------------------------------------------------------------- -// byProvider pro-rata subscription split — pure math, no DB needed -// --------------------------------------------------------------------------- -// The split logic operates on arrays returned by DB queries. -// We test it by calling the actual costService with a mock DB that yields -// controlled query results and verifying the output proportions. - -import { costService } from "../services/index.js"; - -describe("byProvider — pro-rata subscription attribution", () => { - it("splits subscription counts proportionally by token share", async () => { - // Two models: modelA has 75% of tokens, modelB has 25%. - // Total subscription runs = 100, sub input tokens = 1000, sub output tokens = 400. - // Expected: modelA gets 75% of each, modelB gets 25%. - - // We bypass the DB by directly exercising the accumulator math. - // Inline the accumulation logic from costs.ts to verify the arithmetic is correct. - const costRows = [ - { provider: "anthropic", model: "claude-sonnet", costCents: 300, inputTokens: 600, outputTokens: 150 }, - { provider: "anthropic", model: "claude-haiku", costCents: 100, inputTokens: 200, outputTokens: 50 }, - ]; - const subscriptionTotals = { - apiRunCount: 20, - subscriptionRunCount: 100, - subscriptionInputTokens: 1000, - subscriptionOutputTokens: 400, - }; - - const totalTokens = costRows.reduce((s, r) => s + r.inputTokens + r.outputTokens, 0); - // totalTokens = (600+150) + (200+50) = 750 + 250 = 1000 - - const result = costRows.map((row) => { - const rowTokens = row.inputTokens + row.outputTokens; - const share = totalTokens > 0 ? rowTokens / totalTokens : 0; - return { - ...row, - apiRunCount: Math.round(subscriptionTotals.apiRunCount * share), - subscriptionRunCount: Math.round(subscriptionTotals.subscriptionRunCount * share), - subscriptionInputTokens: Math.round(subscriptionTotals.subscriptionInputTokens * share), - subscriptionOutputTokens: Math.round(subscriptionTotals.subscriptionOutputTokens * share), - }; - }); - - // modelA: 750/1000 = 75% - expect(result[0]!.subscriptionRunCount).toBe(75); // 100 * 0.75 - expect(result[0]!.subscriptionInputTokens).toBe(750); // 1000 * 0.75 - expect(result[0]!.subscriptionOutputTokens).toBe(300); // 400 * 0.75 - expect(result[0]!.apiRunCount).toBe(15); // 20 * 0.75 - - // modelB: 250/1000 = 25% - expect(result[1]!.subscriptionRunCount).toBe(25); // 100 * 0.25 - expect(result[1]!.subscriptionInputTokens).toBe(250); // 1000 * 0.25 - expect(result[1]!.subscriptionOutputTokens).toBe(100); // 400 * 0.25 - expect(result[1]!.apiRunCount).toBe(5); // 20 * 0.25 - }); - - it("assigns share=0 to all rows when totalTokens is zero (avoids divide-by-zero)", () => { - const costRows = [ - { provider: "anthropic", model: "claude-sonnet", costCents: 0, inputTokens: 0, outputTokens: 0 }, - { provider: "openai", model: "gpt-5", costCents: 0, inputTokens: 0, outputTokens: 0 }, - ]; - const subscriptionTotals = { apiRunCount: 10, subscriptionRunCount: 5, subscriptionInputTokens: 100, subscriptionOutputTokens: 50 }; - const totalTokens = 0; - - const result = costRows.map((row) => { - const rowTokens = row.inputTokens + row.outputTokens; - const share = totalTokens > 0 ? rowTokens / totalTokens : 0; - return { - subscriptionRunCount: Math.round(subscriptionTotals.subscriptionRunCount * share), - subscriptionInputTokens: Math.round(subscriptionTotals.subscriptionInputTokens * share), - }; - }); - - expect(result[0]!.subscriptionRunCount).toBe(0); - expect(result[0]!.subscriptionInputTokens).toBe(0); - expect(result[1]!.subscriptionRunCount).toBe(0); - expect(result[1]!.subscriptionInputTokens).toBe(0); - }); - - it("attribution rounds to nearest integer (no fractional run counts)", () => { - // 3 models, 10 runs to split — rounding may not sum to exactly 10, that's expected - const costRows = [ - { inputTokens: 1, outputTokens: 0 }, // 1/3 - { inputTokens: 1, outputTokens: 0 }, // 1/3 - { inputTokens: 1, outputTokens: 0 }, // 1/3 - ]; - const totalTokens = 3; - const subscriptionRunCount = 10; - - const result = costRows.map((row) => { - const share = row.inputTokens / totalTokens; - return Math.round(subscriptionRunCount * share); - }); - - // Each should be Math.round(10/3) = Math.round(3.33) = 3 - expect(result).toEqual([3, 3, 3]); - for (const count of result) { - expect(Number.isInteger(count)).toBe(true); - } - }); -}); - -// --------------------------------------------------------------------------- -// windowSpend — verify shape of rolling window results -// --------------------------------------------------------------------------- - -describe("windowSpend — rolling window labels and hours", () => { - it("returns results for the three standard windows (5h, 24h, 7d)", async () => { - // The windowSpend method computes three rolling windows internally. - // We verify the expected window labels exist in a real call by checking - // the service contract shape. Since we're not connecting to a DB here, - // we verify the window definitions directly from service source by - // exercising the label computation inline. - - const windows = [ - { label: "5h", hours: 5 }, - { label: "24h", hours: 24 }, - { label: "7d", hours: 168 }, - ] as const; - - // All three standard windows must be present - expect(windows.map((w) => w.label)).toEqual(["5h", "24h", "7d"]); - // Hours must match expected durations - expect(windows[0]!.hours).toBe(5); - expect(windows[1]!.hours).toBe(24); - expect(windows[2]!.hours).toBe(168); // 7 * 24 + expect(mockFinanceService.summary).toHaveBeenCalled(); + }); + + it("returns 400 for invalid finance event list limits", async () => { + const app = createApp(); + const res = await request(app) + .get("/api/companies/company-1/costs/finance-events") + .query({ limit: "0" }); + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/invalid 'limit'/i); + }); + + it("accepts valid finance event list limits", async () => { + const app = createApp(); + const res = await request(app) + .get("/api/companies/company-1/costs/finance-events") + .query({ limit: "25" }); + expect(res.status).toBe(200); + expect(mockFinanceService.list).toHaveBeenCalledWith("company-1", undefined, 25); }); }); diff --git a/server/src/__tests__/quota-windows-service.test.ts b/server/src/__tests__/quota-windows-service.test.ts new file mode 100644 index 00000000..94d068ad --- /dev/null +++ b/server/src/__tests__/quota-windows-service.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; + +vi.mock("../adapters/registry.js", () => ({ + listServerAdapters: vi.fn(), +})); + +import { listServerAdapters } from "../adapters/registry.js"; +import { fetchAllQuotaWindows } from "../services/quota-windows.js"; + +describe("fetchAllQuotaWindows", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("returns adapter results without waiting for a slower provider to finish forever", async () => { + vi.mocked(listServerAdapters).mockReturnValue([ + { + type: "codex_local", + getQuotaWindows: vi.fn().mockResolvedValue({ + provider: "openai", + source: "codex-rpc", + ok: true, + windows: [{ label: "5h limit", usedPercent: 2, resetsAt: null, valueLabel: null, detail: null }], + }), + }, + { + type: "claude_local", + getQuotaWindows: vi.fn(() => new Promise(() => {})), + }, + ] as never); + + const promise = fetchAllQuotaWindows(); + await vi.advanceTimersByTimeAsync(20_001); + const results = await promise; + + expect(results).toEqual([ + { + provider: "openai", + source: "codex-rpc", + ok: true, + windows: [{ label: "5h limit", usedPercent: 2, resetsAt: null, valueLabel: null, detail: null }], + }, + { + provider: "anthropic", + ok: false, + error: "quota polling timed out after 20s", + windows: [], + }, + ]); + }); +}); diff --git a/server/src/__tests__/quota-windows.test.ts b/server/src/__tests__/quota-windows.test.ts index 7e82f02f..c9d0fd92 100644 --- a/server/src/__tests__/quota-windows.test.ts +++ b/server/src/__tests__/quota-windows.test.ts @@ -8,14 +8,17 @@ import { toPercent, fetchWithTimeout, fetchClaudeQuota, + parseClaudeCliUsageText, readClaudeToken, claudeConfigDir, } from "@paperclipai/adapter-claude-local/server"; import { secondsToWindowLabel, + readCodexAuthInfo, readCodexToken, fetchCodexQuota, + mapCodexRpcQuota, codexHomeDir, } from "@paperclipai/adapter-codex-local/server"; @@ -271,13 +274,86 @@ describe("readClaudeToken", () => { expect(token).toBe("my-test-token"); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); + + it("reads the token from .credentials.json when that is the available Claude auth file", async () => { + const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); + const creds = { claudeAiOauth: { accessToken: "dotfile-token" } }; + await import("node:fs/promises").then((fs) => + fs.mkdir(tmpDir, { recursive: true }).then(() => + fs.writeFile(path.join(tmpDir, ".credentials.json"), JSON.stringify(creds)), + ), + ); + process.env.CLAUDE_CONFIG_DIR = tmpDir; + const token = await readClaudeToken(); + expect(token).toBe("dotfile-token"); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); +}); + +describe("parseClaudeCliUsageText", () => { + it("parses the Claude usage panel layout into quota windows", () => { + const raw = ` + Settings: Status Config Usage + Current session + 2% used + Resets 5pm (America/Chicago) + + Current week (all models) + 47% used + Resets Mar 18 at 7:59am (America/Chicago) + + Current week (Sonnet only) + 0% used + Resets Mar 18 at 8:59am (America/Chicago) + + Extra usage + Extra usage not enabled • /extra-usage to enable + `; + + expect(parseClaudeCliUsageText(raw)).toEqual([ + { + label: "Current session", + usedPercent: 2, + resetsAt: null, + valueLabel: null, + detail: "Resets 5pm (America/Chicago)", + }, + { + label: "Current week (all models)", + usedPercent: 47, + resetsAt: null, + valueLabel: null, + detail: "Resets Mar 18 at 7:59am (America/Chicago)", + }, + { + label: "Current week (Sonnet only)", + usedPercent: 0, + resetsAt: null, + valueLabel: null, + detail: "Resets Mar 18 at 8:59am (America/Chicago)", + }, + { + label: "Extra usage", + usedPercent: null, + resetsAt: null, + valueLabel: null, + detail: "Extra usage not enabled • /extra-usage to enable", + }, + ]); + }); + + it("throws a useful error when the Claude CLI panel reports a usage load failure", () => { + expect(() => parseClaudeCliUsageText("Failed to load usage data")).toThrow( + "Claude CLI could not load usage data. Open the CLI and retry `/usage`.", + ); + }); }); // --------------------------------------------------------------------------- -// readCodexToken — filesystem paths +// readCodexAuthInfo / readCodexToken — filesystem paths // --------------------------------------------------------------------------- -describe("readCodexToken", () => { +describe("readCodexAuthInfo", () => { const savedEnv = process.env.CODEX_HOME; afterEach(() => { @@ -290,7 +366,7 @@ describe("readCodexToken", () => { it("returns null when auth.json does not exist", async () => { process.env.CODEX_HOME = "/tmp/__no_such_paperclip_codex_dir__"; - const result = await readCodexToken(); + const result = await readCodexAuthInfo(); expect(result).toBe(null); }); @@ -302,7 +378,7 @@ describe("readCodexToken", () => { ), ); process.env.CODEX_HOME = tmpDir; - const result = await readCodexToken(); + const result = await readCodexAuthInfo(); expect(result).toBe(null); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); @@ -315,12 +391,12 @@ describe("readCodexToken", () => { ), ); process.env.CODEX_HOME = tmpDir; - const result = await readCodexToken(); + const result = await readCodexAuthInfo(); expect(result).toBe(null); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); - it("returns token and accountId when both are present", async () => { + it("reads the legacy flat auth shape", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`); const auth = { accessToken: "codex-token", accountId: "acc-123" }; await import("node:fs/promises").then((fs) => @@ -329,21 +405,81 @@ describe("readCodexToken", () => { ), ); process.env.CODEX_HOME = tmpDir; - const result = await readCodexToken(); - expect(result).toEqual({ token: "codex-token", accountId: "acc-123" }); + const result = await readCodexAuthInfo(); + expect(result).toMatchObject({ + accessToken: "codex-token", + accountId: "acc-123", + email: null, + planType: null, + }); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); - it("returns token with null accountId when accountId is absent", async () => { + it("reads the modern nested auth shape", async () => { + const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`); + const jwtPayload = Buffer.from( + JSON.stringify({ + email: "codex@example.com", + "https://api.openai.com/auth": { + chatgpt_plan_type: "pro", + chatgpt_user_email: "codex@example.com", + }, + }), + ).toString("base64url"); + const auth = { + tokens: { + access_token: `header.${jwtPayload}.sig`, + account_id: "acc-modern", + refresh_token: "refresh-me", + id_token: `header.${jwtPayload}.sig`, + }, + last_refresh: "2026-03-14T12:00:00Z", + }; + await import("node:fs/promises").then((fs) => + fs.mkdir(tmpDir, { recursive: true }).then(() => + fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify(auth)), + ), + ); + process.env.CODEX_HOME = tmpDir; + const result = await readCodexAuthInfo(); + expect(result).toMatchObject({ + accessToken: `header.${jwtPayload}.sig`, + accountId: "acc-modern", + refreshToken: "refresh-me", + email: "codex@example.com", + planType: "pro", + lastRefresh: "2026-03-14T12:00:00Z", + }); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); +}); + +describe("readCodexToken", () => { + const savedEnv = process.env.CODEX_HOME; + + afterEach(() => { + if (savedEnv === undefined) { + delete process.env.CODEX_HOME; + } else { + process.env.CODEX_HOME = savedEnv; + } + }); + + it("returns token and accountId from the nested auth shape", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`); await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => - fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify({ accessToken: "tok" })), + fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify({ + tokens: { + access_token: "nested-token", + account_id: "acc-nested", + }, + })), ), ); process.env.CODEX_HOME = tmpDir; const result = await readCodexToken(); - expect(result).toEqual({ token: "tok", accountId: null }); + expect(result).toEqual({ token: "nested-token", accountId: "acc-nested" }); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); }); @@ -384,14 +520,22 @@ describe("fetchClaudeQuota", () => { mockFetch({ five_hour: { utilization: 0.4, resets_at: "2026-01-01T00:00:00Z" } }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(1); - expect(windows[0]).toMatchObject({ label: "5h", usedPercent: 40, resetsAt: "2026-01-01T00:00:00Z" }); + expect(windows[0]).toMatchObject({ + label: "Current session", + usedPercent: 40, + resetsAt: "2026-01-01T00:00:00Z", + }); }); it("parses seven_day window", async () => { mockFetch({ seven_day: { utilization: 0.75, resets_at: null } }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(1); - expect(windows[0]).toMatchObject({ label: "7d", usedPercent: 75, resetsAt: null }); + expect(windows[0]).toMatchObject({ + label: "Current week (all models)", + usedPercent: 75, + resetsAt: null, + }); }); it("parses seven_day_sonnet and seven_day_opus windows", async () => { @@ -401,8 +545,8 @@ describe("fetchClaudeQuota", () => { }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(2); - expect(windows[0]!.label).toBe("Sonnet 7d"); - expect(windows[1]!.label).toBe("Opus 7d"); + expect(windows[0]!.label).toBe("Current week (Sonnet only)"); + expect(windows[1]!.label).toBe("Current week (Opus only)"); }); it("sets usedPercent to null when utilization is absent", async () => { @@ -421,7 +565,31 @@ describe("fetchClaudeQuota", () => { const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(4); const labels = windows.map((w: QuotaWindow) => w.label); - expect(labels).toEqual(["5h", "7d", "Sonnet 7d", "Opus 7d"]); + expect(labels).toEqual([ + "Current session", + "Current week (all models)", + "Current week (Sonnet only)", + "Current week (Opus only)", + ]); + }); + + it("parses extra usage when the OAuth response includes it", async () => { + mockFetch({ + extra_usage: { + is_enabled: false, + utilization: null, + }, + }); + const windows = await fetchClaudeQuota("token"); + expect(windows).toEqual([ + { + label: "Extra usage", + usedPercent: null, + resetsAt: null, + valueLabel: "Not enabled", + detail: "Extra usage not enabled", + }, + ]); }); }); @@ -471,15 +639,15 @@ describe("fetchCodexQuota", () => { expect(windows).toEqual([]); }); - it("parses primary_window with 24h label", async () => { + it("normalizes numeric reset timestamps from WHAM", async () => { mockFetch({ rate_limit: { - primary_window: { used_percent: 30, limit_window_seconds: 86400, reset_at: "2026-01-02T00:00:00Z" }, + primary_window: { used_percent: 30, limit_window_seconds: 86400, reset_at: 1_767_312_000 }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows).toHaveLength(1); - expect(windows[0]).toMatchObject({ label: "24h", usedPercent: 30, resetsAt: "2026-01-02T00:00:00Z" }); + expect(windows[0]).toMatchObject({ label: "5h limit", usedPercent: 30, resetsAt: "2026-01-02T00:00:00.000Z" }); }); it("parses secondary_window alongside primary_window", async () => { @@ -491,8 +659,8 @@ describe("fetchCodexQuota", () => { }); const windows = await fetchCodexQuota("token", null); expect(windows).toHaveLength(2); - expect(windows[0]!.label).toBe("5h"); - expect(windows[1]!.label).toBe("7d"); + expect(windows[0]!.label).toBe("5h limit"); + expect(windows[1]!.label).toBe("Weekly limit"); }); it("includes Credits window when credits present and not unlimited", async () => { @@ -521,6 +689,90 @@ describe("fetchCodexQuota", () => { }); }); +describe("mapCodexRpcQuota", () => { + it("maps account and model-specific Codex limits into quota windows", () => { + const snapshot = mapCodexRpcQuota( + { + rateLimits: { + limitId: "codex", + primary: { usedPercent: 1, windowDurationMins: 300, resetsAt: 1_763_500_000 }, + secondary: { usedPercent: 27, windowDurationMins: 10_080 }, + planType: "pro", + }, + rateLimitsByLimitId: { + codex_bengalfox: { + limitId: "codex_bengalfox", + limitName: "GPT-5.3-Codex-Spark", + primary: { usedPercent: 8, windowDurationMins: 300 }, + secondary: { usedPercent: 20, windowDurationMins: 10_080 }, + }, + }, + }, + { + account: { + email: "codex@example.com", + planType: "pro", + }, + }, + ); + + expect(snapshot.email).toBe("codex@example.com"); + expect(snapshot.planType).toBe("pro"); + expect(snapshot.windows).toEqual([ + { + label: "5h limit", + usedPercent: 1, + resetsAt: "2025-11-18T21:06:40.000Z", + valueLabel: null, + detail: null, + }, + { + label: "Weekly limit", + usedPercent: 27, + resetsAt: null, + valueLabel: null, + detail: null, + }, + { + label: "GPT-5.3-Codex-Spark · 5h limit", + usedPercent: 8, + resetsAt: null, + valueLabel: null, + detail: null, + }, + { + label: "GPT-5.3-Codex-Spark · Weekly limit", + usedPercent: 20, + resetsAt: null, + valueLabel: null, + detail: null, + }, + ]); + }); + + it("includes a credits row when the root Codex limit reports finite credits", () => { + const snapshot = mapCodexRpcQuota({ + rateLimits: { + limitId: "codex", + credits: { + unlimited: false, + balance: "12.34", + }, + }, + }); + + expect(snapshot.windows).toEqual([ + { + label: "Credits", + usedPercent: null, + resetsAt: null, + valueLabel: "$12.34 remaining", + detail: null, + }, + ]); + }); +}); + // --------------------------------------------------------------------------- // fetchWithTimeout — abort on timeout // --------------------------------------------------------------------------- diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 6c60b644..46b2af51 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -23,6 +23,7 @@ import { agentService, accessService, approvalService, + budgetService, heartbeatService, issueApprovalService, issueService, @@ -57,6 +58,7 @@ export function agentRoutes(db: Db) { const svc = agentService(db); const access = accessService(db); const approvalsSvc = approvalService(db); + const budgets = budgetService(db); const heartbeat = heartbeatService(db); const issueApprovalsSvc = issueApprovalService(db); const secretsSvc = secretService(db); @@ -941,6 +943,19 @@ export function agentRoutes(db: Db) { details: { name: agent.name, role: agent.role }, }); + if (agent.budgetMonthlyCents > 0) { + await budgets.upsertPolicy( + companyId, + { + scopeType: "agent", + scopeId: agent.id, + amount: agent.budgetMonthlyCents, + windowKind: "calendar_month_utc", + }, + actor.actorType === "user" ? actor.actorId : null, + ); + } + res.status(201).json(agent); }); diff --git a/server/src/routes/companies.ts b/server/src/routes/companies.ts index f034e402..bb6585a2 100644 --- a/server/src/routes/companies.ts +++ b/server/src/routes/companies.ts @@ -9,7 +9,13 @@ import { } from "@paperclipai/shared"; import { forbidden } from "../errors.js"; import { validate } from "../middleware/validate.js"; -import { accessService, companyPortabilityService, companyService, logActivity } from "../services/index.js"; +import { + accessService, + budgetService, + companyPortabilityService, + companyService, + logActivity, +} from "../services/index.js"; import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js"; export function companyRoutes(db: Db) { @@ -17,6 +23,7 @@ export function companyRoutes(db: Db) { const svc = companyService(db); const portability = companyPortabilityService(db); const access = accessService(db); + const budgets = budgetService(db); router.get("/", async (req, res) => { assertBoard(req); @@ -122,6 +129,18 @@ export function companyRoutes(db: Db) { entityId: company.id, details: { name: company.name }, }); + if (company.budgetMonthlyCents > 0) { + await budgets.upsertPolicy( + company.id, + { + scopeType: "company", + scopeId: company.id, + amount: company.budgetMonthlyCents, + windowKind: "calendar_month_utc", + }, + req.actor.userId ?? "board", + ); + } res.status(201).json(company); }); diff --git a/server/src/routes/costs.ts b/server/src/routes/costs.ts index 51bee69d..b61cdad5 100644 --- a/server/src/routes/costs.ts +++ b/server/src/routes/costs.ts @@ -1,8 +1,21 @@ import { Router } from "express"; import type { Db } from "@paperclipai/db"; -import { createCostEventSchema, updateBudgetSchema } from "@paperclipai/shared"; +import { + createCostEventSchema, + createFinanceEventSchema, + resolveBudgetIncidentSchema, + updateBudgetSchema, + upsertBudgetPolicySchema, +} from "@paperclipai/shared"; import { validate } from "../middleware/validate.js"; -import { costService, companyService, agentService, logActivity } from "../services/index.js"; +import { + budgetService, + costService, + financeService, + companyService, + agentService, + logActivity, +} from "../services/index.js"; import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js"; import { fetchAllQuotaWindows } from "../services/quota-windows.js"; import { badRequest } from "../errors.js"; @@ -10,6 +23,8 @@ import { badRequest } from "../errors.js"; export function costRoutes(db: Db) { const router = Router(); const costs = costService(db); + const finance = financeService(db); + const budgets = budgetService(db); const companies = companyService(db); const agents = agentService(db); @@ -42,6 +57,36 @@ export function costRoutes(db: Db) { res.status(201).json(event); }); + router.post("/companies/:companyId/finance-events", validate(createFinanceEventSchema), async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + assertBoard(req); + + const event = await finance.createEvent(companyId, { + ...req.body, + occurredAt: new Date(req.body.occurredAt), + }); + + const actor = getActorInfo(req); + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + action: "finance_event.reported", + entityType: "finance_event", + entityId: event.id, + details: { + amountCents: event.amountCents, + biller: event.biller, + eventKind: event.eventKind, + direction: event.direction, + }, + }); + + res.status(201).json(event); + }); + function parseDateRange(query: Record) { const fromRaw = query.from as string | undefined; const toRaw = query.to as string | undefined; @@ -52,6 +97,16 @@ export function costRoutes(db: Db) { return (from || to) ? { from, to } : undefined; } + function parseLimit(query: Record) { + const raw = query.limit as string | undefined; + if (!raw) return 100; + const limit = Number.parseInt(raw, 10); + if (!Number.isFinite(limit) || limit <= 0 || limit > 500) { + throw badRequest("invalid 'limit' value"); + } + return limit; + } + router.get("/companies/:companyId/costs/summary", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); @@ -84,6 +139,47 @@ export function costRoutes(db: Db) { res.json(rows); }); + router.get("/companies/:companyId/costs/by-biller", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const range = parseDateRange(req.query); + const rows = await costs.byBiller(companyId, range); + res.json(rows); + }); + + router.get("/companies/:companyId/costs/finance-summary", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const range = parseDateRange(req.query); + const summary = await finance.summary(companyId, range); + res.json(summary); + }); + + router.get("/companies/:companyId/costs/finance-by-biller", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const range = parseDateRange(req.query); + const rows = await finance.byBiller(companyId, range); + res.json(rows); + }); + + router.get("/companies/:companyId/costs/finance-by-kind", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const range = parseDateRange(req.query); + const rows = await finance.byKind(companyId, range); + res.json(rows); + }); + + router.get("/companies/:companyId/costs/finance-events", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const range = parseDateRange(req.query); + const limit = parseLimit(req.query); + const rows = await finance.list(companyId, range, limit); + res.json(rows); + }); + router.get("/companies/:companyId/costs/window-spend", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); @@ -106,6 +202,38 @@ export function costRoutes(db: Db) { res.json(results); }); + router.get("/companies/:companyId/budgets/overview", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const overview = await budgets.overview(companyId); + res.json(overview); + }); + + router.post( + "/companies/:companyId/budgets/policies", + validate(upsertBudgetPolicySchema), + async (req, res) => { + assertBoard(req); + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const summary = await budgets.upsertPolicy(companyId, req.body, req.actor.userId ?? "board"); + res.json(summary); + }, + ); + + router.post( + "/companies/:companyId/budget-incidents/:incidentId/resolve", + validate(resolveBudgetIncidentSchema), + async (req, res) => { + assertBoard(req); + const companyId = req.params.companyId as string; + const incidentId = req.params.incidentId as string; + assertCompanyAccess(req, companyId); + const incident = await budgets.resolveIncident(companyId, incidentId, req.body, req.actor.userId ?? "board"); + res.json(incident); + }, + ); + router.get("/companies/:companyId/costs/by-project", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); @@ -133,6 +261,17 @@ export function costRoutes(db: Db) { details: { budgetMonthlyCents: req.body.budgetMonthlyCents }, }); + await budgets.upsertPolicy( + companyId, + { + scopeType: "company", + scopeId: companyId, + amount: req.body.budgetMonthlyCents, + windowKind: "calendar_month_utc", + }, + req.actor.userId ?? "board", + ); + res.json(company); }); @@ -169,6 +308,17 @@ export function costRoutes(db: Db) { details: { budgetMonthlyCents: updated.budgetMonthlyCents }, }); + await budgets.upsertPolicy( + updated.companyId, + { + scopeType: "agent", + scopeId: updated.id, + amount: updated.budgetMonthlyCents, + windowKind: "calendar_month_utc", + }, + req.actor.type === "board" ? req.actor.userId ?? "board" : null, + ); + res.json(updated); }); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index c745277b..92325e2c 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -921,6 +921,19 @@ export function issueRoutes(db: Db, storage: StorageService) { } assertCompanyAccess(req, issue.companyId); + if (issue.projectId) { + const project = await projectsSvc.getById(issue.projectId); + if (project?.pausedAt) { + res.status(409).json({ + error: + project.pauseReason === "budget" + ? "Project is paused because its budget hard-stop was reached" + : "Project is paused", + }); + return; + } + } + if (req.actor.type === "agent" && req.actor.agentId !== req.body.agentId) { res.status(403).json({ error: "Agent can only checkout as itself" }); return; diff --git a/server/src/services/agents.ts b/server/src/services/agents.ts index fa65c7e4..4daa1dd9 100644 --- a/server/src/services/agents.ts +++ b/server/src/services/agents.ts @@ -360,14 +360,19 @@ export function agentService(db: Db) { update: updateAgent, - pause: async (id: string) => { + pause: async (id: string, reason: "manual" | "budget" | "system" = "manual") => { const existing = await getById(id); if (!existing) return null; if (existing.status === "terminated") throw conflict("Cannot pause terminated agent"); const updated = await db .update(agents) - .set({ status: "paused", updatedAt: new Date() }) + .set({ + status: "paused", + pauseReason: reason, + pausedAt: new Date(), + updatedAt: new Date(), + }) .where(eq(agents.id, id)) .returning() .then((rows) => rows[0] ?? null); @@ -384,7 +389,12 @@ export function agentService(db: Db) { const updated = await db .update(agents) - .set({ status: "idle", updatedAt: new Date() }) + .set({ + status: "idle", + pauseReason: null, + pausedAt: null, + updatedAt: new Date(), + }) .where(eq(agents.id, id)) .returning() .then((rows) => rows[0] ?? null); @@ -397,7 +407,12 @@ export function agentService(db: Db) { await db .update(agents) - .set({ status: "terminated", updatedAt: new Date() }) + .set({ + status: "terminated", + pauseReason: null, + pausedAt: null, + updatedAt: new Date(), + }) .where(eq(agents.id, id)); await db diff --git a/server/src/services/approvals.ts b/server/src/services/approvals.ts index 53833044..f2bdb227 100644 --- a/server/src/services/approvals.ts +++ b/server/src/services/approvals.ts @@ -4,6 +4,7 @@ import { approvalComments, approvals } from "@paperclipai/db"; import { notFound, unprocessable } from "../errors.js"; import { redactCurrentUserText } from "../log-redaction.js"; import { agentService } from "./agents.js"; +import { budgetService } from "./budgets.js"; import { notifyHireApproved } from "./hire-hook.js"; function redactApprovalComment(comment: T): T { @@ -15,6 +16,7 @@ function redactApprovalComment(comment: T): T { export function approvalService(db: Db) { const agentsSvc = agentService(db); + const budgets = budgetService(db); const canResolveStatuses = new Set(["pending", "revision_requested"]); const resolvableStatuses = Array.from(canResolveStatuses); type ApprovalRecord = typeof approvals.$inferSelect; @@ -137,6 +139,20 @@ export function approvalService(db: Db) { hireApprovedAgentId = created?.id ?? null; } if (hireApprovedAgentId) { + const budgetMonthlyCents = + typeof payload.budgetMonthlyCents === "number" ? payload.budgetMonthlyCents : 0; + if (budgetMonthlyCents > 0) { + await budgets.upsertPolicy( + updated.companyId, + { + scopeType: "agent", + scopeId: hireApprovedAgentId, + amount: budgetMonthlyCents, + windowKind: "calendar_month_utc", + }, + decidedByUserId, + ); + } void notifyHireApproved(db, { companyId: updated.companyId, agentId: hireApprovedAgentId, diff --git a/server/src/services/budgets.ts b/server/src/services/budgets.ts new file mode 100644 index 00000000..cccf2a20 --- /dev/null +++ b/server/src/services/budgets.ts @@ -0,0 +1,919 @@ +import { and, desc, eq, gte, inArray, lt, sql } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { + agents, + approvals, + budgetIncidents, + budgetPolicies, + companies, + costEvents, + projects, +} from "@paperclipai/db"; +import type { + BudgetIncident, + BudgetIncidentResolutionInput, + BudgetMetric, + BudgetOverview, + BudgetPolicy, + BudgetPolicySummary, + BudgetPolicyUpsertInput, + BudgetScopeType, + BudgetThresholdType, + BudgetWindowKind, +} from "@paperclipai/shared"; +import { notFound, unprocessable } from "../errors.js"; +import { logActivity } from "./activity-log.js"; + +type ScopeRecord = { + companyId: string; + name: string; + paused: boolean; + pauseReason: "manual" | "budget" | "system" | null; +}; + +type PolicyRow = typeof budgetPolicies.$inferSelect; +type IncidentRow = typeof budgetIncidents.$inferSelect; + +function currentUtcMonthWindow(now = new Date()) { + const year = now.getUTCFullYear(); + const month = now.getUTCMonth(); + const start = new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)); + const end = new Date(Date.UTC(year, month + 1, 1, 0, 0, 0, 0)); + return { start, end }; +} + +function resolveWindow(windowKind: BudgetWindowKind, now = new Date()) { + if (windowKind === "lifetime") { + return { + start: new Date(Date.UTC(1970, 0, 1, 0, 0, 0, 0)), + end: new Date(Date.UTC(9999, 0, 1, 0, 0, 0, 0)), + }; + } + return currentUtcMonthWindow(now); +} + +function budgetStatusFromObserved( + observedAmount: number, + amount: number, + warnPercent: number, +): BudgetPolicySummary["status"] { + if (amount <= 0) return "ok"; + if (observedAmount >= amount) return "hard_stop"; + if (observedAmount >= Math.ceil((amount * warnPercent) / 100)) return "warning"; + return "ok"; +} + +function normalizeScopeName(scopeType: BudgetScopeType, name: string) { + if (scopeType === "company") return name; + return name.trim().length > 0 ? name : scopeType; +} + +async function resolveScopeRecord(db: Db, scopeType: BudgetScopeType, scopeId: string): Promise { + if (scopeType === "company") { + const row = await db + .select({ + companyId: companies.id, + name: companies.name, + status: companies.status, + }) + .from(companies) + .where(eq(companies.id, scopeId)) + .then((rows) => rows[0] ?? null); + if (!row) throw notFound("Company not found"); + return { + companyId: row.companyId, + name: row.name, + paused: row.status === "paused", + pauseReason: row.status === "paused" ? "budget" : null, + }; + } + + if (scopeType === "agent") { + const row = await db + .select({ + companyId: agents.companyId, + name: agents.name, + status: agents.status, + pauseReason: agents.pauseReason, + }) + .from(agents) + .where(eq(agents.id, scopeId)) + .then((rows) => rows[0] ?? null); + if (!row) throw notFound("Agent not found"); + return { + companyId: row.companyId, + name: row.name, + paused: row.status === "paused", + pauseReason: (row.pauseReason as ScopeRecord["pauseReason"]) ?? null, + }; + } + + const row = await db + .select({ + companyId: projects.companyId, + name: projects.name, + pauseReason: projects.pauseReason, + pausedAt: projects.pausedAt, + }) + .from(projects) + .where(eq(projects.id, scopeId)) + .then((rows) => rows[0] ?? null); + if (!row) throw notFound("Project not found"); + return { + companyId: row.companyId, + name: row.name, + paused: Boolean(row.pausedAt), + pauseReason: (row.pauseReason as ScopeRecord["pauseReason"]) ?? null, + }; +} + +async function computeObservedAmount( + db: Db, + policy: Pick, +) { + if (policy.metric !== "billed_cents") return 0; + + const conditions = [eq(costEvents.companyId, policy.companyId)]; + if (policy.scopeType === "agent") conditions.push(eq(costEvents.agentId, policy.scopeId)); + if (policy.scopeType === "project") conditions.push(eq(costEvents.projectId, policy.scopeId)); + const { start, end } = resolveWindow(policy.windowKind as BudgetWindowKind); + if (policy.windowKind === "calendar_month_utc") { + conditions.push(gte(costEvents.occurredAt, start)); + conditions.push(lt(costEvents.occurredAt, end)); + } + + const [row] = await db + .select({ + total: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, + }) + .from(costEvents) + .where(and(...conditions)); + + return Number(row?.total ?? 0); +} + +function buildApprovalPayload(input: { + policy: PolicyRow; + scopeName: string; + thresholdType: BudgetThresholdType; + amountObserved: number; + windowStart: Date; + windowEnd: Date; +}) { + return { + scopeType: input.policy.scopeType, + scopeId: input.policy.scopeId, + scopeName: input.scopeName, + metric: input.policy.metric, + windowKind: input.policy.windowKind, + thresholdType: input.thresholdType, + budgetAmount: input.policy.amount, + observedAmount: input.amountObserved, + warnPercent: input.policy.warnPercent, + windowStart: input.windowStart.toISOString(), + windowEnd: input.windowEnd.toISOString(), + policyId: input.policy.id, + guidance: "Raise the budget and resume the scope, or keep the scope paused.", + }; +} + +async function markApprovalStatus( + db: Db, + approvalId: string | null, + status: "approved" | "rejected", + decisionNote: string | null | undefined, + decidedByUserId: string, +) { + if (!approvalId) return; + await db + .update(approvals) + .set({ + status, + decisionNote: decisionNote ?? null, + decidedByUserId, + decidedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(approvals.id, approvalId)); +} + +export function budgetService(db: Db) { + async function pauseScopeForBudget(policy: PolicyRow) { + const now = new Date(); + if (policy.scopeType === "agent") { + await db + .update(agents) + .set({ + status: "paused", + pauseReason: "budget", + pausedAt: now, + updatedAt: now, + }) + .where(and(eq(agents.id, policy.scopeId), inArray(agents.status, ["active", "idle", "running", "error"]))); + return; + } + + if (policy.scopeType === "project") { + await db + .update(projects) + .set({ + pauseReason: "budget", + pausedAt: now, + updatedAt: now, + }) + .where(eq(projects.id, policy.scopeId)); + return; + } + + await db + .update(companies) + .set({ + status: "paused", + updatedAt: now, + }) + .where(eq(companies.id, policy.scopeId)); + } + + async function resumeScopeFromBudget(policy: PolicyRow) { + const now = new Date(); + if (policy.scopeType === "agent") { + await db + .update(agents) + .set({ + status: "idle", + pauseReason: null, + pausedAt: null, + updatedAt: now, + }) + .where(and(eq(agents.id, policy.scopeId), eq(agents.pauseReason, "budget"))); + return; + } + + if (policy.scopeType === "project") { + await db + .update(projects) + .set({ + pauseReason: null, + pausedAt: null, + updatedAt: now, + }) + .where(and(eq(projects.id, policy.scopeId), eq(projects.pauseReason, "budget"))); + return; + } + + await db + .update(companies) + .set({ + status: "active", + updatedAt: now, + }) + .where(eq(companies.id, policy.scopeId)); + } + + async function getPolicyRow(policyId: string) { + const policy = await db + .select() + .from(budgetPolicies) + .where(eq(budgetPolicies.id, policyId)) + .then((rows) => rows[0] ?? null); + if (!policy) throw notFound("Budget policy not found"); + return policy; + } + + async function listPolicyRows(companyId: string) { + return db + .select() + .from(budgetPolicies) + .where(eq(budgetPolicies.companyId, companyId)) + .orderBy(desc(budgetPolicies.updatedAt)); + } + + async function buildPolicySummary(policy: PolicyRow): Promise { + const scope = await resolveScopeRecord(db, policy.scopeType as BudgetScopeType, policy.scopeId); + const observedAmount = await computeObservedAmount(db, policy); + const { start, end } = resolveWindow(policy.windowKind as BudgetWindowKind); + const amount = policy.isActive ? policy.amount : 0; + const utilizationPercent = + amount > 0 ? Number(((observedAmount / amount) * 100).toFixed(2)) : 0; + return { + policyId: policy.id, + companyId: policy.companyId, + scopeType: policy.scopeType as BudgetScopeType, + scopeId: policy.scopeId, + scopeName: normalizeScopeName(policy.scopeType as BudgetScopeType, scope.name), + metric: policy.metric as BudgetMetric, + windowKind: policy.windowKind as BudgetWindowKind, + amount, + observedAmount, + remainingAmount: amount > 0 ? Math.max(0, amount - observedAmount) : 0, + utilizationPercent, + warnPercent: policy.warnPercent, + hardStopEnabled: policy.hardStopEnabled, + notifyEnabled: policy.notifyEnabled, + isActive: policy.isActive, + status: policy.isActive + ? budgetStatusFromObserved(observedAmount, amount, policy.warnPercent) + : "ok", + paused: scope.paused, + pauseReason: scope.pauseReason, + windowStart: start, + windowEnd: end, + }; + } + + async function createIncidentIfNeeded( + policy: PolicyRow, + thresholdType: BudgetThresholdType, + amountObserved: number, + ) { + const { start, end } = resolveWindow(policy.windowKind as BudgetWindowKind); + const existing = await db + .select() + .from(budgetIncidents) + .where( + and( + eq(budgetIncidents.policyId, policy.id), + eq(budgetIncidents.windowStart, start), + eq(budgetIncidents.thresholdType, thresholdType), + ), + ) + .then((rows) => rows[0] ?? null); + if (existing) return existing; + + const scope = await resolveScopeRecord(db, policy.scopeType as BudgetScopeType, policy.scopeId); + const payload = buildApprovalPayload({ + policy, + scopeName: normalizeScopeName(policy.scopeType as BudgetScopeType, scope.name), + thresholdType, + amountObserved, + windowStart: start, + windowEnd: end, + }); + + const approval = thresholdType === "hard" + ? await db + .insert(approvals) + .values({ + companyId: policy.companyId, + type: "budget_override_required", + requestedByUserId: null, + requestedByAgentId: null, + status: "pending", + payload, + }) + .returning() + .then((rows) => rows[0] ?? null) + : null; + + return db + .insert(budgetIncidents) + .values({ + companyId: policy.companyId, + policyId: policy.id, + scopeType: policy.scopeType, + scopeId: policy.scopeId, + metric: policy.metric, + windowKind: policy.windowKind, + windowStart: start, + windowEnd: end, + thresholdType, + amountLimit: policy.amount, + amountObserved, + status: "open", + approvalId: approval?.id ?? null, + }) + .returning() + .then((rows) => rows[0] ?? null); + } + + async function resolveOpenSoftIncidents(policyId: string) { + await db + .update(budgetIncidents) + .set({ + status: "resolved", + resolvedAt: new Date(), + updatedAt: new Date(), + }) + .where( + and( + eq(budgetIncidents.policyId, policyId), + eq(budgetIncidents.thresholdType, "soft"), + eq(budgetIncidents.status, "open"), + ), + ); + } + + async function resolveOpenIncidentsForPolicy( + policyId: string, + approvalStatus: "approved" | "rejected" | null, + decidedByUserId: string | null, + ) { + const openRows = await db + .select() + .from(budgetIncidents) + .where(and(eq(budgetIncidents.policyId, policyId), eq(budgetIncidents.status, "open"))); + + await db + .update(budgetIncidents) + .set({ + status: "resolved", + resolvedAt: new Date(), + updatedAt: new Date(), + }) + .where(and(eq(budgetIncidents.policyId, policyId), eq(budgetIncidents.status, "open"))); + + if (!approvalStatus || !decidedByUserId) return; + for (const row of openRows) { + await markApprovalStatus(db, row.approvalId ?? null, approvalStatus, "Resolved via budget update", decidedByUserId); + } + } + + async function hydrateIncidentRows(rows: IncidentRow[]): Promise { + const approvalIds = rows.map((row) => row.approvalId).filter((value): value is string => Boolean(value)); + const approvalRows = approvalIds.length > 0 + ? await db + .select({ id: approvals.id, status: approvals.status }) + .from(approvals) + .where(inArray(approvals.id, approvalIds)) + : []; + const approvalStatusById = new Map(approvalRows.map((row) => [row.id, row.status])); + + return Promise.all( + rows.map(async (row) => { + const scope = await resolveScopeRecord(db, row.scopeType as BudgetScopeType, row.scopeId); + return { + id: row.id, + companyId: row.companyId, + policyId: row.policyId, + scopeType: row.scopeType as BudgetScopeType, + scopeId: row.scopeId, + scopeName: normalizeScopeName(row.scopeType as BudgetScopeType, scope.name), + metric: row.metric as BudgetMetric, + windowKind: row.windowKind as BudgetWindowKind, + windowStart: row.windowStart, + windowEnd: row.windowEnd, + thresholdType: row.thresholdType as BudgetThresholdType, + amountLimit: row.amountLimit, + amountObserved: row.amountObserved, + status: row.status as BudgetIncident["status"], + approvalId: row.approvalId ?? null, + approvalStatus: row.approvalId ? approvalStatusById.get(row.approvalId) ?? null : null, + resolvedAt: row.resolvedAt ?? null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; + }), + ); + } + + return { + listPolicies: async (companyId: string): Promise => { + const rows = await listPolicyRows(companyId); + return rows.map((row) => ({ + ...row, + scopeType: row.scopeType as BudgetScopeType, + metric: row.metric as BudgetMetric, + windowKind: row.windowKind as BudgetWindowKind, + })); + }, + + upsertPolicy: async ( + companyId: string, + input: BudgetPolicyUpsertInput, + actorUserId: string | null, + ): Promise => { + const scope = await resolveScopeRecord(db, input.scopeType, input.scopeId); + if (scope.companyId !== companyId) { + throw unprocessable("Budget scope does not belong to company"); + } + + const metric = input.metric ?? "billed_cents"; + const windowKind = input.windowKind ?? (input.scopeType === "project" ? "lifetime" : "calendar_month_utc"); + const amount = Math.max(0, Math.floor(input.amount)); + const nextIsActive = amount > 0 && (input.isActive ?? true); + const existing = await db + .select() + .from(budgetPolicies) + .where( + and( + eq(budgetPolicies.companyId, companyId), + eq(budgetPolicies.scopeType, input.scopeType), + eq(budgetPolicies.scopeId, input.scopeId), + eq(budgetPolicies.metric, metric), + eq(budgetPolicies.windowKind, windowKind), + ), + ) + .then((rows) => rows[0] ?? null); + + const now = new Date(); + const row = existing + ? await db + .update(budgetPolicies) + .set({ + amount, + warnPercent: input.warnPercent ?? existing.warnPercent, + hardStopEnabled: input.hardStopEnabled ?? existing.hardStopEnabled, + notifyEnabled: input.notifyEnabled ?? existing.notifyEnabled, + isActive: nextIsActive, + updatedByUserId: actorUserId, + updatedAt: now, + }) + .where(eq(budgetPolicies.id, existing.id)) + .returning() + .then((rows) => rows[0]) + : await db + .insert(budgetPolicies) + .values({ + companyId, + scopeType: input.scopeType, + scopeId: input.scopeId, + metric, + windowKind, + amount, + warnPercent: input.warnPercent ?? 80, + hardStopEnabled: input.hardStopEnabled ?? true, + notifyEnabled: input.notifyEnabled ?? true, + isActive: nextIsActive, + createdByUserId: actorUserId, + updatedByUserId: actorUserId, + }) + .returning() + .then((rows) => rows[0]); + + if (input.scopeType === "company" && windowKind === "calendar_month_utc") { + await db + .update(companies) + .set({ + budgetMonthlyCents: amount, + updatedAt: now, + }) + .where(eq(companies.id, input.scopeId)); + } + + if (input.scopeType === "agent" && windowKind === "calendar_month_utc") { + await db + .update(agents) + .set({ + budgetMonthlyCents: amount, + updatedAt: now, + }) + .where(eq(agents.id, input.scopeId)); + } + + if (amount > 0) { + const observedAmount = await computeObservedAmount(db, row); + if (observedAmount < amount) { + await resumeScopeFromBudget(row); + await resolveOpenIncidentsForPolicy(row.id, actorUserId ? "approved" : null, actorUserId); + } else { + const softThreshold = Math.ceil((row.amount * row.warnPercent) / 100); + if (row.notifyEnabled && observedAmount >= softThreshold) { + await createIncidentIfNeeded(row, "soft", observedAmount); + } + if (row.hardStopEnabled && observedAmount >= row.amount) { + await resolveOpenSoftIncidents(row.id); + await createIncidentIfNeeded(row, "hard", observedAmount); + await pauseScopeForBudget(row); + } + } + } else { + await resumeScopeFromBudget(row); + await resolveOpenIncidentsForPolicy(row.id, actorUserId ? "approved" : null, actorUserId); + } + + await logActivity(db, { + companyId, + actorType: "user", + actorId: actorUserId ?? "board", + action: "budget.policy_upserted", + entityType: "budget_policy", + entityId: row.id, + details: { + scopeType: row.scopeType, + scopeId: row.scopeId, + amount: row.amount, + windowKind: row.windowKind, + }, + }); + + return buildPolicySummary(row); + }, + + overview: async (companyId: string): Promise => { + const rows = await listPolicyRows(companyId); + const policies = await Promise.all(rows.map((row) => buildPolicySummary(row))); + const activeIncidentRows = await db + .select() + .from(budgetIncidents) + .where(and(eq(budgetIncidents.companyId, companyId), eq(budgetIncidents.status, "open"))) + .orderBy(desc(budgetIncidents.createdAt)); + const activeIncidents = await hydrateIncidentRows(activeIncidentRows); + return { + companyId, + policies, + activeIncidents, + pausedAgentCount: policies.filter((policy) => policy.scopeType === "agent" && policy.paused).length, + pausedProjectCount: policies.filter((policy) => policy.scopeType === "project" && policy.paused).length, + pendingApprovalCount: activeIncidents.filter((incident) => incident.approvalStatus === "pending").length, + }; + }, + + evaluateCostEvent: async (event: typeof costEvents.$inferSelect) => { + const candidatePolicies = await db + .select() + .from(budgetPolicies) + .where( + and( + eq(budgetPolicies.companyId, event.companyId), + eq(budgetPolicies.isActive, true), + inArray(budgetPolicies.scopeType, ["company", "agent", "project"]), + ), + ); + + const relevantPolicies = candidatePolicies.filter((policy) => { + if (policy.scopeType === "company") return policy.scopeId === event.companyId; + if (policy.scopeType === "agent") return policy.scopeId === event.agentId; + if (policy.scopeType === "project") return Boolean(event.projectId) && policy.scopeId === event.projectId; + return false; + }); + + for (const policy of relevantPolicies) { + if (policy.metric !== "billed_cents" || policy.amount <= 0) continue; + const observedAmount = await computeObservedAmount(db, policy); + const softThreshold = Math.ceil((policy.amount * policy.warnPercent) / 100); + + if (policy.notifyEnabled && observedAmount >= softThreshold) { + const softIncident = await createIncidentIfNeeded(policy, "soft", observedAmount); + if (softIncident) { + await logActivity(db, { + companyId: policy.companyId, + actorType: "system", + actorId: "budget_service", + action: "budget.soft_threshold_crossed", + entityType: "budget_incident", + entityId: softIncident.id, + details: { + scopeType: policy.scopeType, + scopeId: policy.scopeId, + amountObserved: observedAmount, + amountLimit: policy.amount, + }, + }); + } + } + + if (policy.hardStopEnabled && observedAmount >= policy.amount) { + await resolveOpenSoftIncidents(policy.id); + const hardIncident = await createIncidentIfNeeded(policy, "hard", observedAmount); + await pauseScopeForBudget(policy); + if (hardIncident) { + await logActivity(db, { + companyId: policy.companyId, + actorType: "system", + actorId: "budget_service", + action: "budget.hard_threshold_crossed", + entityType: "budget_incident", + entityId: hardIncident.id, + details: { + scopeType: policy.scopeType, + scopeId: policy.scopeId, + amountObserved: observedAmount, + amountLimit: policy.amount, + approvalId: hardIncident.approvalId ?? null, + }, + }); + } + } + } + }, + + getInvocationBlock: async ( + companyId: string, + agentId: string, + context?: { issueId?: string | null; projectId?: string | null }, + ) => { + const agent = await db + .select({ + status: agents.status, + pauseReason: agents.pauseReason, + companyId: agents.companyId, + name: agents.name, + }) + .from(agents) + .where(eq(agents.id, agentId)) + .then((rows) => rows[0] ?? null); + if (!agent || agent.companyId !== companyId) throw notFound("Agent not found"); + + const company = await db + .select({ + status: companies.status, + name: companies.name, + }) + .from(companies) + .where(eq(companies.id, companyId)) + .then((rows) => rows[0] ?? null); + if (!company) throw notFound("Company not found"); + if (company.status === "paused") { + return { + scopeType: "company" as const, + scopeId: companyId, + scopeName: company.name, + reason: "Company is paused and cannot start new work.", + }; + } + + const companyPolicy = await db + .select() + .from(budgetPolicies) + .where( + and( + eq(budgetPolicies.companyId, companyId), + eq(budgetPolicies.scopeType, "company"), + eq(budgetPolicies.scopeId, companyId), + eq(budgetPolicies.isActive, true), + eq(budgetPolicies.metric, "billed_cents"), + ), + ) + .then((rows) => rows[0] ?? null); + if (companyPolicy && companyPolicy.hardStopEnabled && companyPolicy.amount > 0) { + const observed = await computeObservedAmount(db, companyPolicy); + if (observed >= companyPolicy.amount) { + return { + scopeType: "company" as const, + scopeId: companyId, + scopeName: company.name, + reason: "Company cannot start new work because its budget hard-stop is exceeded.", + }; + } + } + + if (agent.status === "paused" && agent.pauseReason === "budget") { + return { + scopeType: "agent" as const, + scopeId: agentId, + scopeName: agent.name, + reason: "Agent is paused because its budget hard-stop was reached.", + }; + } + + const agentPolicy = await db + .select() + .from(budgetPolicies) + .where( + and( + eq(budgetPolicies.companyId, companyId), + eq(budgetPolicies.scopeType, "agent"), + eq(budgetPolicies.scopeId, agentId), + eq(budgetPolicies.isActive, true), + eq(budgetPolicies.metric, "billed_cents"), + ), + ) + .then((rows) => rows[0] ?? null); + if (agentPolicy && agentPolicy.hardStopEnabled && agentPolicy.amount > 0) { + const observed = await computeObservedAmount(db, agentPolicy); + if (observed >= agentPolicy.amount) { + return { + scopeType: "agent" as const, + scopeId: agentId, + scopeName: agent.name, + reason: "Agent cannot start because its budget hard-stop is still exceeded.", + }; + } + } + + const candidateProjectId = context?.projectId ?? null; + if (!candidateProjectId) return null; + + const project = await db + .select({ + id: projects.id, + name: projects.name, + companyId: projects.companyId, + pauseReason: projects.pauseReason, + pausedAt: projects.pausedAt, + }) + .from(projects) + .where(eq(projects.id, candidateProjectId)) + .then((rows) => rows[0] ?? null); + + if (!project || project.companyId !== companyId) return null; + const projectPolicy = await db + .select() + .from(budgetPolicies) + .where( + and( + eq(budgetPolicies.companyId, companyId), + eq(budgetPolicies.scopeType, "project"), + eq(budgetPolicies.scopeId, project.id), + eq(budgetPolicies.isActive, true), + eq(budgetPolicies.metric, "billed_cents"), + ), + ) + .then((rows) => rows[0] ?? null); + if (projectPolicy && projectPolicy.hardStopEnabled && projectPolicy.amount > 0) { + const observed = await computeObservedAmount(db, projectPolicy); + if (observed >= projectPolicy.amount) { + return { + scopeType: "project" as const, + scopeId: project.id, + scopeName: project.name, + reason: "Project cannot start work because its budget hard-stop is still exceeded.", + }; + } + } + + if (!project.pausedAt || project.pauseReason !== "budget") return null; + return { + scopeType: "project" as const, + scopeId: project.id, + scopeName: project.name, + reason: "Project is paused because its budget hard-stop was reached.", + }; + }, + + resolveIncident: async ( + companyId: string, + incidentId: string, + input: BudgetIncidentResolutionInput, + actorUserId: string, + ): Promise => { + const incident = await db + .select() + .from(budgetIncidents) + .where(eq(budgetIncidents.id, incidentId)) + .then((rows) => rows[0] ?? null); + if (!incident) throw notFound("Budget incident not found"); + if (incident.companyId !== companyId) throw notFound("Budget incident not found"); + + const policy = await getPolicyRow(incident.policyId); + if (input.action === "raise_budget_and_resume") { + const nextAmount = Math.max(0, Math.floor(input.amount ?? 0)); + if (nextAmount <= incident.amountObserved) { + throw unprocessable("New budget must exceed current observed spend"); + } + + await db + .update(budgetPolicies) + .set({ + amount: nextAmount, + isActive: true, + updatedByUserId: actorUserId, + updatedAt: new Date(), + }) + .where(eq(budgetPolicies.id, policy.id)); + + if (policy.scopeType === "agent" && policy.windowKind === "calendar_month_utc") { + await db + .update(agents) + .set({ budgetMonthlyCents: nextAmount, updatedAt: new Date() }) + .where(eq(agents.id, policy.scopeId)); + } + + await resumeScopeFromBudget(policy); + await db + .update(budgetIncidents) + .set({ + status: "resolved", + resolvedAt: new Date(), + updatedAt: new Date(), + }) + .where(and(eq(budgetIncidents.policyId, policy.id), eq(budgetIncidents.status, "open"))); + + await markApprovalStatus(db, incident.approvalId ?? null, "approved", input.decisionNote, actorUserId); + } else { + await db + .update(budgetIncidents) + .set({ + status: "dismissed", + resolvedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(budgetIncidents.id, incident.id)); + await markApprovalStatus(db, incident.approvalId ?? null, "rejected", input.decisionNote, actorUserId); + } + + await logActivity(db, { + companyId: incident.companyId, + actorType: "user", + actorId: actorUserId, + action: "budget.incident_resolved", + entityType: "budget_incident", + entityId: incident.id, + details: { + action: input.action, + amount: input.amount ?? null, + scopeType: incident.scopeType, + scopeId: incident.scopeId, + }, + }); + + const [updated] = await hydrateIncidentRows([{ + ...incident, + status: input.action === "raise_budget_and_resume" ? "resolved" : "dismissed", + resolvedAt: new Date(), + updatedAt: new Date(), + }]); + return updated!; + }, + }; +} diff --git a/server/src/services/companies.ts b/server/src/services/companies.ts index 42c4e972..7fafb093 100644 --- a/server/src/services/companies.ts +++ b/server/src/services/companies.ts @@ -16,6 +16,7 @@ import { heartbeatRuns, heartbeatRunEvents, costEvents, + financeEvents, approvalComments, approvals, activityLog, @@ -206,6 +207,7 @@ export function companyService(db: Db) { await tx.delete(agentRuntimeState).where(eq(agentRuntimeState.companyId, id)); await tx.delete(issueComments).where(eq(issueComments.companyId, id)); await tx.delete(costEvents).where(eq(costEvents.companyId, id)); + await tx.delete(financeEvents).where(eq(financeEvents.companyId, id)); await tx.delete(approvalComments).where(eq(approvalComments.companyId, id)); await tx.delete(approvals).where(eq(approvals.companyId, id)); await tx.delete(companySecrets).where(eq(companySecrets.companyId, id)); diff --git a/server/src/services/costs.ts b/server/src/services/costs.ts index 4874fb84..528d2678 100644 --- a/server/src/services/costs.ts +++ b/server/src/services/costs.ts @@ -1,14 +1,19 @@ import { and, desc, eq, gte, isNotNull, lte, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; -import { activityLog, agents, companies, costEvents, heartbeatRuns, issues, projects } from "@paperclipai/db"; +import { activityLog, agents, companies, costEvents, issues, projects } from "@paperclipai/db"; import { notFound, unprocessable } from "../errors.js"; +import { budgetService } from "./budgets.js"; export interface CostDateRange { from?: Date; to?: Date; } +const METERED_BILLING_TYPE = "metered_api"; +const SUBSCRIPTION_BILLING_TYPES = ["subscription_included", "subscription_overage"] as const; + export function costService(db: Db) { + const budgets = budgetService(db); return { createEvent: async (companyId: string, data: Omit) => { const agent = await db @@ -24,7 +29,13 @@ export function costService(db: Db) { const event = await db .insert(costEvents) - .values({ ...data, companyId }) + .values({ + ...data, + companyId, + biller: data.biller ?? data.provider, + billingType: data.billingType ?? "unknown", + cachedInputTokens: data.cachedInputTokens ?? 0, + }) .returning() .then((rows) => rows[0]); @@ -63,6 +74,8 @@ export function costService(db: Db) { .where(eq(agents.id, updatedAgent.id)); } + await budgets.evaluateCostEvent(event); + return event; }, @@ -105,52 +118,31 @@ export function costService(db: Db) { if (range?.from) conditions.push(gte(costEvents.occurredAt, range.from)); if (range?.to) conditions.push(lte(costEvents.occurredAt, range.to)); - const costRows = await db + return db .select({ agentId: costEvents.agentId, agentName: agents.name, agentStatus: agents.status, costCents: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, inputTokens: sql`coalesce(sum(${costEvents.inputTokens}), 0)::int`, + cachedInputTokens: sql`coalesce(sum(${costEvents.cachedInputTokens}), 0)::int`, outputTokens: sql`coalesce(sum(${costEvents.outputTokens}), 0)::int`, + apiRunCount: + sql`count(distinct case when ${costEvents.billingType} = ${METERED_BILLING_TYPE} then ${costEvents.heartbeatRunId} end)::int`, + subscriptionRunCount: + sql`count(distinct case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.heartbeatRunId} end)::int`, + subscriptionCachedInputTokens: + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.cachedInputTokens} else 0 end), 0)::int`, + subscriptionInputTokens: + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.inputTokens} else 0 end), 0)::int`, + subscriptionOutputTokens: + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.outputTokens} else 0 end), 0)::int`, }) .from(costEvents) .leftJoin(agents, eq(costEvents.agentId, agents.id)) .where(and(...conditions)) .groupBy(costEvents.agentId, agents.name, agents.status) .orderBy(desc(sql`coalesce(sum(${costEvents.costCents}), 0)::int`)); - - const runConditions: ReturnType[] = [eq(heartbeatRuns.companyId, companyId)]; - if (range?.from) runConditions.push(gte(heartbeatRuns.startedAt, range.from)); - if (range?.to) runConditions.push(lte(heartbeatRuns.startedAt, range.to)); - - const runRows = await db - .select({ - agentId: heartbeatRuns.agentId, - apiRunCount: - sql`coalesce(sum(case when coalesce((${heartbeatRuns.usageJson} ->> 'billingType'), 'unknown') = 'api' then 1 else 0 end), 0)::int`, - subscriptionRunCount: - sql`coalesce(sum(case when coalesce((${heartbeatRuns.usageJson} ->> 'billingType'), 'unknown') = 'subscription' then 1 else 0 end), 0)::int`, - subscriptionInputTokens: - sql`coalesce(sum(case when coalesce((${heartbeatRuns.usageJson} ->> 'billingType'), 'unknown') = 'subscription' then coalesce((${heartbeatRuns.usageJson} ->> 'inputTokens')::int, 0) else 0 end), 0)::int`, - subscriptionOutputTokens: - sql`coalesce(sum(case when coalesce((${heartbeatRuns.usageJson} ->> 'billingType'), 'unknown') = 'subscription' then coalesce((${heartbeatRuns.usageJson} ->> 'outputTokens')::int, 0) else 0 end), 0)::int`, - }) - .from(heartbeatRuns) - .where(and(...runConditions)) - .groupBy(heartbeatRuns.agentId); - - const runRowsByAgent = new Map(runRows.map((row) => [row.agentId, row])); - return costRows.map((row) => { - const runRow = runRowsByAgent.get(row.agentId); - return { - ...row, - apiRunCount: runRow?.apiRunCount ?? 0, - subscriptionRunCount: runRow?.subscriptionRunCount ?? 0, - subscriptionInputTokens: runRow?.subscriptionInputTokens ?? 0, - subscriptionOutputTokens: runRow?.subscriptionOutputTokens ?? 0, - }; - }); }, byProvider: async (companyId: string, range?: CostDateRange) => { @@ -158,68 +150,62 @@ export function costService(db: Db) { if (range?.from) conditions.push(gte(costEvents.occurredAt, range.from)); if (range?.to) conditions.push(lte(costEvents.occurredAt, range.to)); - const costRows = await db + return db .select({ provider: costEvents.provider, + biller: costEvents.biller, + billingType: costEvents.billingType, model: costEvents.model, costCents: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, inputTokens: sql`coalesce(sum(${costEvents.inputTokens}), 0)::int`, + cachedInputTokens: sql`coalesce(sum(${costEvents.cachedInputTokens}), 0)::int`, outputTokens: sql`coalesce(sum(${costEvents.outputTokens}), 0)::int`, + apiRunCount: + sql`count(distinct case when ${costEvents.billingType} = ${METERED_BILLING_TYPE} then ${costEvents.heartbeatRunId} end)::int`, + subscriptionRunCount: + sql`count(distinct case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.heartbeatRunId} end)::int`, + subscriptionCachedInputTokens: + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.cachedInputTokens} else 0 end), 0)::int`, + subscriptionInputTokens: + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.inputTokens} else 0 end), 0)::int`, + subscriptionOutputTokens: + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.outputTokens} else 0 end), 0)::int`, }) .from(costEvents) .where(and(...conditions)) - .groupBy(costEvents.provider, costEvents.model) + .groupBy(costEvents.provider, costEvents.biller, costEvents.billingType, costEvents.model) .orderBy(desc(sql`coalesce(sum(${costEvents.costCents}), 0)::int`)); + }, - const runConditions: ReturnType[] = [eq(heartbeatRuns.companyId, companyId)]; - if (range?.from) runConditions.push(gte(heartbeatRuns.startedAt, range.from)); - if (range?.to) runConditions.push(lte(heartbeatRuns.startedAt, range.to)); + byBiller: async (companyId: string, range?: CostDateRange) => { + const conditions: ReturnType[] = [eq(costEvents.companyId, companyId)]; + if (range?.from) conditions.push(gte(costEvents.occurredAt, range.from)); + if (range?.to) conditions.push(lte(costEvents.occurredAt, range.to)); - const runRows = await db + return db .select({ - agentId: heartbeatRuns.agentId, + biller: costEvents.biller, + costCents: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, + inputTokens: sql`coalesce(sum(${costEvents.inputTokens}), 0)::int`, + cachedInputTokens: sql`coalesce(sum(${costEvents.cachedInputTokens}), 0)::int`, + outputTokens: sql`coalesce(sum(${costEvents.outputTokens}), 0)::int`, apiRunCount: - sql`coalesce(sum(case when coalesce((${heartbeatRuns.usageJson} ->> 'billingType'), 'unknown') = 'api' then 1 else 0 end), 0)::int`, + sql`count(distinct case when ${costEvents.billingType} = ${METERED_BILLING_TYPE} then ${costEvents.heartbeatRunId} end)::int`, subscriptionRunCount: - sql`coalesce(sum(case when coalesce((${heartbeatRuns.usageJson} ->> 'billingType'), 'unknown') = 'subscription' then 1 else 0 end), 0)::int`, + sql`count(distinct case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.heartbeatRunId} end)::int`, + subscriptionCachedInputTokens: + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.cachedInputTokens} else 0 end), 0)::int`, subscriptionInputTokens: - sql`coalesce(sum(case when coalesce((${heartbeatRuns.usageJson} ->> 'billingType'), 'unknown') = 'subscription' then coalesce((${heartbeatRuns.usageJson} ->> 'inputTokens')::int, 0) else 0 end), 0)::int`, + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.inputTokens} else 0 end), 0)::int`, subscriptionOutputTokens: - sql`coalesce(sum(case when coalesce((${heartbeatRuns.usageJson} ->> 'billingType'), 'unknown') = 'subscription' then coalesce((${heartbeatRuns.usageJson} ->> 'outputTokens')::int, 0) else 0 end), 0)::int`, + sql`coalesce(sum(case when ${costEvents.billingType} in (${sql.join(SUBSCRIPTION_BILLING_TYPES.map((value) => sql`${value}`), sql`, `)}) then ${costEvents.outputTokens} else 0 end), 0)::int`, + providerCount: sql`count(distinct ${costEvents.provider})::int`, + modelCount: sql`count(distinct ${costEvents.model})::int`, }) - .from(heartbeatRuns) - .where(and(...runConditions)) - .groupBy(heartbeatRuns.agentId); - - // aggregate run billing splits across all agents (runs don't carry model info so we can't go per-model) - const totals = runRows.reduce( - (acc, r) => ({ - apiRunCount: acc.apiRunCount + r.apiRunCount, - subscriptionRunCount: acc.subscriptionRunCount + r.subscriptionRunCount, - subscriptionInputTokens: acc.subscriptionInputTokens + r.subscriptionInputTokens, - subscriptionOutputTokens: acc.subscriptionOutputTokens + r.subscriptionOutputTokens, - }), - { apiRunCount: 0, subscriptionRunCount: 0, subscriptionInputTokens: 0, subscriptionOutputTokens: 0 }, - ); - - // pro-rate billing split across models by token share - const totalTokens = costRows.reduce((s, r) => s + r.inputTokens + r.outputTokens, 0); - - return costRows.map((row) => { - const rowTokens = row.inputTokens + row.outputTokens; - const share = totalTokens > 0 ? rowTokens / totalTokens : 0; - return { - provider: row.provider, - model: row.model, - costCents: row.costCents, - inputTokens: row.inputTokens, - outputTokens: row.outputTokens, - apiRunCount: Math.round(totals.apiRunCount * share), - subscriptionRunCount: Math.round(totals.subscriptionRunCount * share), - subscriptionInputTokens: Math.round(totals.subscriptionInputTokens * share), - subscriptionOutputTokens: Math.round(totals.subscriptionOutputTokens * share), - }; - }); + .from(costEvents) + .where(and(...conditions)) + .groupBy(costEvents.biller) + .orderBy(desc(sql`coalesce(sum(${costEvents.costCents}), 0)::int`)); }, /** @@ -240,8 +226,10 @@ export function costService(db: Db) { const rows = await db .select({ provider: costEvents.provider, + biller: sql`case when count(distinct ${costEvents.biller}) = 1 then min(${costEvents.biller}) else 'mixed' end`, costCents: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, inputTokens: sql`coalesce(sum(${costEvents.inputTokens}), 0)::int`, + cachedInputTokens: sql`coalesce(sum(${costEvents.cachedInputTokens}), 0)::int`, outputTokens: sql`coalesce(sum(${costEvents.outputTokens}), 0)::int`, }) .from(costEvents) @@ -256,10 +244,12 @@ export function costService(db: Db) { return rows.map((row) => ({ provider: row.provider, + biller: row.biller, window: label as string, windowHours: hours, costCents: row.costCents, inputTokens: row.inputTokens, + cachedInputTokens: row.cachedInputTokens, outputTokens: row.outputTokens, })); }), @@ -282,16 +272,26 @@ export function costService(db: Db) { agentId: costEvents.agentId, agentName: agents.name, provider: costEvents.provider, + biller: costEvents.biller, + billingType: costEvents.billingType, model: costEvents.model, costCents: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, inputTokens: sql`coalesce(sum(${costEvents.inputTokens}), 0)::int`, + cachedInputTokens: sql`coalesce(sum(${costEvents.cachedInputTokens}), 0)::int`, outputTokens: sql`coalesce(sum(${costEvents.outputTokens}), 0)::int`, }) .from(costEvents) .leftJoin(agents, eq(costEvents.agentId, agents.id)) .where(and(...conditions)) - .groupBy(costEvents.agentId, agents.name, costEvents.provider, costEvents.model) - .orderBy(costEvents.provider, costEvents.model); + .groupBy( + costEvents.agentId, + agents.name, + costEvents.provider, + costEvents.biller, + costEvents.billingType, + costEvents.model, + ) + .orderBy(costEvents.provider, costEvents.biller, costEvents.billingType, costEvents.model); }, byProject: async (companyId: string, range?: CostDateRange) => { @@ -320,25 +320,27 @@ export function costService(db: Db) { .orderBy(activityLog.runId, issues.projectId, desc(activityLog.createdAt)) .as("run_project_links"); - const conditions: ReturnType[] = [eq(heartbeatRuns.companyId, companyId)]; - if (range?.from) conditions.push(gte(heartbeatRuns.startedAt, range.from)); - if (range?.to) conditions.push(lte(heartbeatRuns.startedAt, range.to)); + const effectiveProjectId = sql`coalesce(${costEvents.projectId}, ${runProjectLinks.projectId})`; + const conditions: ReturnType[] = [eq(costEvents.companyId, companyId)]; + if (range?.from) conditions.push(gte(costEvents.occurredAt, range.from)); + if (range?.to) conditions.push(lte(costEvents.occurredAt, range.to)); - const costCentsExpr = sql`coalesce(sum(round(coalesce((${heartbeatRuns.usageJson} ->> 'costUsd')::numeric, 0) * 100)), 0)::int`; + const costCentsExpr = sql`coalesce(sum(${costEvents.costCents}), 0)::int`; return db .select({ - projectId: runProjectLinks.projectId, + projectId: effectiveProjectId, projectName: projects.name, costCents: costCentsExpr, - inputTokens: sql`coalesce(sum(coalesce((${heartbeatRuns.usageJson} ->> 'inputTokens')::int, 0)), 0)::int`, - outputTokens: sql`coalesce(sum(coalesce((${heartbeatRuns.usageJson} ->> 'outputTokens')::int, 0)), 0)::int`, + inputTokens: sql`coalesce(sum(${costEvents.inputTokens}), 0)::int`, + cachedInputTokens: sql`coalesce(sum(${costEvents.cachedInputTokens}), 0)::int`, + outputTokens: sql`coalesce(sum(${costEvents.outputTokens}), 0)::int`, }) - .from(runProjectLinks) - .innerJoin(heartbeatRuns, eq(runProjectLinks.runId, heartbeatRuns.id)) - .innerJoin(projects, eq(runProjectLinks.projectId, projects.id)) - .where(and(...conditions)) - .groupBy(runProjectLinks.projectId, projects.name) + .from(costEvents) + .leftJoin(runProjectLinks, eq(costEvents.heartbeatRunId, runProjectLinks.runId)) + .innerJoin(projects, sql`${projects.id} = ${effectiveProjectId}`) + .where(and(...conditions, sql`${effectiveProjectId} is not null`)) + .groupBy(effectiveProjectId, projects.name) .orderBy(desc(costCentsExpr)); }, }; diff --git a/server/src/services/dashboard.ts b/server/src/services/dashboard.ts index 991c9c61..e495208d 100644 --- a/server/src/services/dashboard.ts +++ b/server/src/services/dashboard.ts @@ -2,8 +2,10 @@ import { and, eq, gte, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents, approvals, companies, costEvents, issues } from "@paperclipai/db"; import { notFound } from "../errors.js"; +import { budgetService } from "./budgets.js"; export function dashboardService(db: Db) { + const budgets = budgetService(db); return { summary: async (companyId: string) => { const company = await db @@ -78,6 +80,7 @@ export function dashboardService(db: Db) { company.budgetMonthlyCents > 0 ? (monthSpendCents / company.budgetMonthlyCents) * 100 : 0; + const budgetOverview = await budgets.overview(companyId); return { companyId, @@ -94,6 +97,12 @@ export function dashboardService(db: Db) { monthUtilizationPercent: Number(utilization.toFixed(2)), }, pendingApprovals, + budgets: { + activeIncidents: budgetOverview.activeIncidents.length, + pendingApprovals: budgetOverview.pendingApprovalCount, + pausedAgents: budgetOverview.pausedAgentCount, + pausedProjects: budgetOverview.pausedProjectCount, + }, }; }, }; diff --git a/server/src/services/finance.ts b/server/src/services/finance.ts new file mode 100644 index 00000000..29dcec49 --- /dev/null +++ b/server/src/services/finance.ts @@ -0,0 +1,134 @@ +import { and, desc, eq, gte, lte, sql } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { agents, costEvents, financeEvents, goals, heartbeatRuns, issues, projects } from "@paperclipai/db"; +import { notFound, unprocessable } from "../errors.js"; + +export interface FinanceDateRange { + from?: Date; + to?: Date; +} + +async function assertBelongsToCompany( + db: Db, + table: any, + id: string, + companyId: string, + label: string, +) { + const row = await db + .select() + .from(table) + .where(eq(table.id, id)) + .then((rows) => rows[0] ?? null); + + if (!row) throw notFound(`${label} not found`); + if ((row as unknown as { companyId: string }).companyId !== companyId) { + throw unprocessable(`${label} does not belong to company`); + } +} + +function rangeConditions(companyId: string, range?: FinanceDateRange) { + const conditions: ReturnType[] = [eq(financeEvents.companyId, companyId)]; + if (range?.from) conditions.push(gte(financeEvents.occurredAt, range.from)); + if (range?.to) conditions.push(lte(financeEvents.occurredAt, range.to)); + return conditions; +} + +export function financeService(db: Db) { + const debitExpr = sql`coalesce(sum(case when ${financeEvents.direction} = 'debit' then ${financeEvents.amountCents} else 0 end), 0)::int`; + const creditExpr = sql`coalesce(sum(case when ${financeEvents.direction} = 'credit' then ${financeEvents.amountCents} else 0 end), 0)::int`; + const estimatedDebitExpr = sql`coalesce(sum(case when ${financeEvents.direction} = 'debit' and ${financeEvents.estimated} = true then ${financeEvents.amountCents} else 0 end), 0)::int`; + + return { + createEvent: async (companyId: string, data: Omit) => { + if (data.agentId) await assertBelongsToCompany(db, agents, data.agentId, companyId, "Agent"); + if (data.issueId) await assertBelongsToCompany(db, issues, data.issueId, companyId, "Issue"); + if (data.projectId) await assertBelongsToCompany(db, projects, data.projectId, companyId, "Project"); + if (data.goalId) await assertBelongsToCompany(db, goals, data.goalId, companyId, "Goal"); + if (data.heartbeatRunId) await assertBelongsToCompany(db, heartbeatRuns, data.heartbeatRunId, companyId, "Heartbeat run"); + if (data.costEventId) await assertBelongsToCompany(db, costEvents, data.costEventId, companyId, "Cost event"); + + const event = await db + .insert(financeEvents) + .values({ + ...data, + companyId, + currency: data.currency ?? "USD", + direction: data.direction ?? "debit", + estimated: data.estimated ?? false, + }) + .returning() + .then((rows) => rows[0]); + + return event; + }, + + summary: async (companyId: string, range?: FinanceDateRange) => { + const conditions = rangeConditions(companyId, range); + const [row] = await db + .select({ + debitCents: debitExpr, + creditCents: creditExpr, + estimatedDebitCents: estimatedDebitExpr, + eventCount: sql`count(*)::int`, + }) + .from(financeEvents) + .where(and(...conditions)); + + return { + companyId, + debitCents: Number(row?.debitCents ?? 0), + creditCents: Number(row?.creditCents ?? 0), + netCents: Number(row?.debitCents ?? 0) - Number(row?.creditCents ?? 0), + estimatedDebitCents: Number(row?.estimatedDebitCents ?? 0), + eventCount: Number(row?.eventCount ?? 0), + }; + }, + + byBiller: async (companyId: string, range?: FinanceDateRange) => { + const conditions = rangeConditions(companyId, range); + return db + .select({ + biller: financeEvents.biller, + debitCents: debitExpr, + creditCents: creditExpr, + estimatedDebitCents: estimatedDebitExpr, + eventCount: sql`count(*)::int`, + kindCount: sql`count(distinct ${financeEvents.eventKind})::int`, + netCents: sql`(${debitExpr} - ${creditExpr})::int`, + }) + .from(financeEvents) + .where(and(...conditions)) + .groupBy(financeEvents.biller) + .orderBy(desc(sql`(${debitExpr} - ${creditExpr})::int`), financeEvents.biller); + }, + + byKind: async (companyId: string, range?: FinanceDateRange) => { + const conditions = rangeConditions(companyId, range); + return db + .select({ + eventKind: financeEvents.eventKind, + debitCents: debitExpr, + creditCents: creditExpr, + estimatedDebitCents: estimatedDebitExpr, + eventCount: sql`count(*)::int`, + billerCount: sql`count(distinct ${financeEvents.biller})::int`, + netCents: sql`(${debitExpr} - ${creditExpr})::int`, + }) + .from(financeEvents) + .where(and(...conditions)) + .groupBy(financeEvents.eventKind) + .orderBy(desc(sql`(${debitExpr} - ${creditExpr})::int`), financeEvents.eventKind); + }, + + list: async (companyId: string, range?: FinanceDateRange, limit: number = 100) => { + const conditions = rangeConditions(companyId, range); + return db + .select() + .from(financeEvents) + .where(and(...conditions)) + .orderBy(desc(financeEvents.occurredAt), desc(financeEvents.createdAt)) + .limit(limit); + }, + }; +} diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index e924359c..e1424c65 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { and, asc, desc, eq, gt, inArray, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; +import type { BillingType } from "@paperclipai/shared"; import { agents, agentRuntimeState, @@ -22,6 +23,7 @@ import type { AdapterExecutionResult, AdapterInvocationMeta, AdapterSessionCodec import { createLocalAgentJwt } from "../agent-auth-jwt.js"; import { parseObject, asBoolean, asNumber, appendWithCap, MAX_EXCERPT_BYTES } from "../adapters/utils.js"; import { costService } from "./costs.js"; +import { budgetService } from "./budgets.js"; import { secretService } from "./secrets.js"; import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js"; import { summarizeHeartbeatRunResultJson } from "./heartbeat-run-summary.js"; @@ -170,6 +172,67 @@ function readNonEmptyString(value: unknown): string | null { return typeof value === "string" && value.trim().length > 0 ? value : null; } +function normalizeLedgerBillingType(value: unknown): BillingType { + const raw = readNonEmptyString(value); + switch (raw) { + case "api": + case "metered_api": + return "metered_api"; + case "subscription": + case "subscription_included": + return "subscription_included"; + case "subscription_overage": + return "subscription_overage"; + case "credits": + return "credits"; + case "fixed": + return "fixed"; + default: + return "unknown"; + } +} + +function resolveLedgerBiller(result: AdapterExecutionResult): string { + return readNonEmptyString(result.biller) ?? readNonEmptyString(result.provider) ?? "unknown"; +} + +function normalizeBilledCostCents(costUsd: number | null | undefined, billingType: BillingType): number { + if (billingType === "subscription_included") return 0; + if (typeof costUsd !== "number" || !Number.isFinite(costUsd)) return 0; + return Math.max(0, Math.round(costUsd * 100)); +} + +async function resolveLedgerScopeForRun( + db: Db, + companyId: string, + run: typeof heartbeatRuns.$inferSelect, +) { + const context = parseObject(run.contextSnapshot); + const contextIssueId = readNonEmptyString(context.issueId); + const contextProjectId = readNonEmptyString(context.projectId); + + if (!contextIssueId) { + return { + issueId: null, + projectId: contextProjectId, + }; + } + + const issue = await db + .select({ + id: issues.id, + projectId: issues.projectId, + }) + .from(issues) + .where(and(eq(issues.id, contextIssueId), eq(issues.companyId, companyId))) + .then((rows) => rows[0] ?? null); + + return { + issueId: issue?.id ?? null, + projectId: issue?.projectId ?? contextProjectId, + }; +} + function normalizeUsageTotals(usage: UsageSummary | null | undefined): UsageTotals | null { if (!usage) return null; return { @@ -554,6 +617,7 @@ function resolveNextSessionState(input: { export function heartbeatService(db: Db) { const runLogStore = getRunLogStore(); + const budgets = budgetService(db); const secretsSvc = secretService(db); const issuesSvc = issueService(db); const activeRunExecutions = new Set(); @@ -1294,8 +1358,12 @@ export function heartbeatService(db: Db) { const inputTokens = usage?.inputTokens ?? 0; const outputTokens = usage?.outputTokens ?? 0; const cachedInputTokens = usage?.cachedInputTokens ?? 0; - const additionalCostCents = Math.max(0, Math.round((result.costUsd ?? 0) * 100)); + const billingType = normalizeLedgerBillingType(result.billingType); + const additionalCostCents = normalizeBilledCostCents(result.costUsd, billingType); const hasTokenUsage = inputTokens > 0 || outputTokens > 0 || cachedInputTokens > 0; + const provider = result.provider ?? "unknown"; + const biller = resolveLedgerBiller(result); + const ledgerScope = await resolveLedgerScopeForRun(db, agent.companyId, run); await db .update(agentRuntimeState) @@ -1316,10 +1384,16 @@ export function heartbeatService(db: Db) { if (additionalCostCents > 0 || hasTokenUsage) { const costs = costService(db); await costs.createEvent(agent.companyId, { + heartbeatRunId: run.id, agentId: agent.id, - provider: result.provider ?? "unknown", + issueId: ledgerScope.issueId, + projectId: ledgerScope.projectId, + provider, + biller, + billingType, model: result.model ?? "unknown", inputTokens, + cachedInputTokens, outputTokens, costCents: additionalCostCents, occurredAt: new Date(), @@ -1875,8 +1949,11 @@ export function heartbeatService(db: Db) { freshSession: runtimeForAdapter.sessionId == null && runtimeForAdapter.sessionDisplayId == null, sessionRotated: sessionCompaction.rotate, sessionRotationReason: sessionCompaction.reason, + provider: readNonEmptyString(adapterResult.provider) ?? "unknown", + biller: resolveLedgerBiller(adapterResult), + model: readNonEmptyString(adapterResult.model) ?? "unknown", ...(adapterResult.costUsd != null ? { costUsd: adapterResult.costUsd } : {}), - ...(adapterResult.billingType ? { billingType: adapterResult.billingType } : {}), + billingType: normalizeLedgerBillingType(adapterResult.billingType), } as Record) : null; @@ -2226,6 +2303,43 @@ export function heartbeatService(db: Db) { const agent = await getAgent(agentId); if (!agent) throw notFound("Agent not found"); + const writeSkippedRequest = async (skipReason: string) => { + await db.insert(agentWakeupRequests).values({ + companyId: agent.companyId, + agentId, + source, + triggerDetail, + reason: skipReason, + payload, + status: "skipped", + requestedByActorType: opts.requestedByActorType ?? null, + requestedByActorId: opts.requestedByActorId ?? null, + idempotencyKey: opts.idempotencyKey ?? null, + finishedAt: new Date(), + }); + }; + + let projectId = readNonEmptyString(enrichedContextSnapshot.projectId); + if (!projectId && issueId) { + projectId = await db + .select({ projectId: issues.projectId }) + .from(issues) + .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId))) + .then((rows) => rows[0]?.projectId ?? null); + } + + const budgetBlock = await budgets.getInvocationBlock(agent.companyId, agentId, { + issueId, + projectId, + }); + if (budgetBlock) { + await writeSkippedRequest("budget.blocked"); + throw conflict(budgetBlock.reason, { + scopeType: budgetBlock.scopeType, + scopeId: budgetBlock.scopeId, + }); + } + if ( agent.status === "paused" || agent.status === "terminated" || @@ -2235,21 +2349,6 @@ export function heartbeatService(db: Db) { } const policy = parseHeartbeatPolicy(agent); - const writeSkippedRequest = async (reason: string) => { - await db.insert(agentWakeupRequests).values({ - companyId: agent.companyId, - agentId, - source, - triggerDetail, - reason, - payload, - status: "skipped", - requestedByActorType: opts.requestedByActorType ?? null, - requestedByActorId: opts.requestedByActorId ?? null, - idempotencyKey: opts.idempotencyKey ?? null, - finishedAt: new Date(), - }); - }; if (source === "timer" && !policy.enabled) { await writeSkippedRequest("heartbeat.disabled"); diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 625a0ac5..9c16e709 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -8,8 +8,10 @@ export { issueApprovalService } from "./issue-approvals.js"; export { goalService } from "./goals.js"; export { activityService, type ActivityFilters } from "./activity.js"; export { approvalService } from "./approvals.js"; +export { budgetService } from "./budgets.js"; export { secretService } from "./secrets.js"; export { costService } from "./costs.js"; +export { financeService } from "./finance.js"; export { heartbeatService } from "./heartbeat.js"; export { dashboardService } from "./dashboard.js"; export { sidebarBadgeService } from "./sidebar-badges.js"; diff --git a/server/src/services/quota-windows.ts b/server/src/services/quota-windows.ts index 868f6be7..664406ab 100644 --- a/server/src/services/quota-windows.ts +++ b/server/src/services/quota-windows.ts @@ -1,6 +1,19 @@ import type { ProviderQuotaResult } from "@paperclipai/shared"; import { listServerAdapters } from "../adapters/registry.js"; +const QUOTA_PROVIDER_TIMEOUT_MS = 20_000; + +function providerSlugForAdapterType(type: string): string { + switch (type) { + case "claude_local": + return "anthropic"; + case "codex_local": + return "openai"; + default: + return type; + } +} + /** * Asks each registered adapter for its provider quota windows and aggregates the results. * Adapters that don't implement getQuotaWindows() are silently skipped. @@ -11,19 +24,41 @@ export async function fetchAllQuotaWindows(): Promise { const adapters = listServerAdapters().filter((a) => a.getQuotaWindows != null); const settled = await Promise.allSettled( - adapters.map((adapter) => adapter.getQuotaWindows!()), + adapters.map((adapter) => withQuotaTimeout(adapter.type, adapter.getQuotaWindows!())), ); return settled.map((result, i) => { if (result.status === "fulfilled") return result.value; - // Determine provider slug from the fulfilled value if available, otherwise fall back - // to the adapter type so the error is still attributable to the right provider. const adapterType = adapters[i]!.type; return { - provider: adapterType, + provider: providerSlugForAdapterType(adapterType), ok: false, error: String(result.reason), windows: [], }; }); } + +async function withQuotaTimeout( + adapterType: string, + task: Promise, +): Promise { + let timeoutId: NodeJS.Timeout | null = null; + try { + return await Promise.race([ + task, + new Promise((resolve) => { + timeoutId = setTimeout(() => { + resolve({ + provider: providerSlugForAdapterType(adapterType), + ok: false, + error: `quota polling timed out after ${Math.round(QUOTA_PROVIDER_TIMEOUT_MS / 1000)}s`, + windows: [], + }); + }, QUOTA_PROVIDER_TIMEOUT_MS); + }), + ]); + } finally { + if (timeoutId) clearTimeout(timeoutId); + } +} diff --git a/ui/src/api/budgets.ts b/ui/src/api/budgets.ts new file mode 100644 index 00000000..342617b1 --- /dev/null +++ b/ui/src/api/budgets.ts @@ -0,0 +1,20 @@ +import type { + BudgetIncident, + BudgetIncidentResolutionInput, + BudgetOverview, + BudgetPolicySummary, + BudgetPolicyUpsertInput, +} from "@paperclipai/shared"; +import { api } from "./client"; + +export const budgetsApi = { + overview: (companyId: string) => + api.get(`/companies/${companyId}/budgets/overview`), + upsertPolicy: (companyId: string, data: BudgetPolicyUpsertInput) => + api.post(`/companies/${companyId}/budgets/policies`, data), + resolveIncident: (companyId: string, incidentId: string, data: BudgetIncidentResolutionInput) => + api.post( + `/companies/${companyId}/budget-incidents/${encodeURIComponent(incidentId)}/resolve`, + data, + ), +}; diff --git a/ui/src/api/costs.ts b/ui/src/api/costs.ts index 104a1e56..4dda60a7 100644 --- a/ui/src/api/costs.ts +++ b/ui/src/api/costs.ts @@ -1,4 +1,17 @@ -import type { CostSummary, CostByAgent, CostByProviderModel, CostByAgentModel, CostByProject, CostWindowSpendRow, ProviderQuotaResult } from "@paperclipai/shared"; +import type { + CostSummary, + CostByAgent, + CostByProviderModel, + CostByBiller, + CostByAgentModel, + CostByProject, + CostWindowSpendRow, + FinanceSummary, + FinanceByBiller, + FinanceByKind, + FinanceEvent, + ProviderQuotaResult, +} from "@paperclipai/shared"; import { api } from "./client"; function dateParams(from?: string, to?: string): string { @@ -20,8 +33,27 @@ export const costsApi = { api.get(`/companies/${companyId}/costs/by-project${dateParams(from, to)}`), byProvider: (companyId: string, from?: string, to?: string) => api.get(`/companies/${companyId}/costs/by-provider${dateParams(from, to)}`), + byBiller: (companyId: string, from?: string, to?: string) => + api.get(`/companies/${companyId}/costs/by-biller${dateParams(from, to)}`), + financeSummary: (companyId: string, from?: string, to?: string) => + api.get(`/companies/${companyId}/costs/finance-summary${dateParams(from, to)}`), + financeByBiller: (companyId: string, from?: string, to?: string) => + api.get(`/companies/${companyId}/costs/finance-by-biller${dateParams(from, to)}`), + financeByKind: (companyId: string, from?: string, to?: string) => + api.get(`/companies/${companyId}/costs/finance-by-kind${dateParams(from, to)}`), + financeEvents: (companyId: string, from?: string, to?: string, limit: number = 100) => + api.get(`/companies/${companyId}/costs/finance-events${dateParamsWithLimit(from, to, limit)}`), windowSpend: (companyId: string) => api.get(`/companies/${companyId}/costs/window-spend`), quotaWindows: (companyId: string) => api.get(`/companies/${companyId}/costs/quota-windows`), }; + +function dateParamsWithLimit(from?: string, to?: string, limit?: number): string { + const params = new URLSearchParams(); + if (from) params.set("from", from); + if (to) params.set("to", to); + if (limit) params.set("limit", String(limit)); + const qs = params.toString(); + return qs ? `?${qs}` : ""; +} diff --git a/ui/src/components/AccountingModelCard.tsx b/ui/src/components/AccountingModelCard.tsx new file mode 100644 index 00000000..98704061 --- /dev/null +++ b/ui/src/components/AccountingModelCard.tsx @@ -0,0 +1,69 @@ +import { Database, Gauge, ReceiptText } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; + +const SURFACES = [ + { + title: "Inference ledger", + description: "Request-scoped usage and billed runs from cost_events.", + icon: Database, + points: ["tokens + billed dollars", "provider, biller, model", "subscription and overage aware"], + tone: "from-sky-500/12 via-sky-500/6 to-transparent", + }, + { + title: "Finance ledger", + description: "Account-level charges that are not one prompt-response pair.", + icon: ReceiptText, + points: ["top-ups, refunds, fees", "Bedrock provisioned or training charges", "credit expiries and adjustments"], + tone: "from-amber-500/14 via-amber-500/6 to-transparent", + }, + { + title: "Live quotas", + description: "Provider or biller windows that can stop traffic in real time.", + icon: Gauge, + points: ["provider quota windows", "biller credit systems", "errors surfaced directly"], + tone: "from-emerald-500/14 via-emerald-500/6 to-transparent", + }, +] as const; + +export function AccountingModelCard() { + return ( + +
+ + + Accounting model + + + Paperclip now separates request-level inference usage from account-level finance events. + That keeps provider reporting honest when the biller is OpenRouter, Cloudflare, Bedrock, or another intermediary. + + + + {SURFACES.map((surface) => { + const Icon = surface.icon; + return ( +
+
+
+ +
+
+
{surface.title}
+
{surface.description}
+
+
+
+ {surface.points.map((point) => ( +
{point}
+ ))} +
+
+ ); + })} +
+ + ); +} diff --git a/ui/src/components/ApprovalCard.tsx b/ui/src/components/ApprovalCard.tsx index 2a1e6e82..ee0a4163 100644 --- a/ui/src/components/ApprovalCard.tsx +++ b/ui/src/components/ApprovalCard.tsx @@ -33,6 +33,9 @@ export function ApprovalCard({ }) { const Icon = typeIcon[approval.type] ?? defaultTypeIcon; const label = typeLabel[approval.type] ?? approval.type; + const showResolutionButtons = + approval.type !== "budget_override_required" && + (approval.status === "pending" || approval.status === "revision_requested"); return (
@@ -67,7 +70,7 @@ export function ApprovalCard({ )} {/* Actions */} - {(approval.status === "pending" || approval.status === "revision_requested") && ( + {showResolutionButtons && (
+
+ {parsed !== null && parsed <= incident.amountObserved ? ( +

+ The new budget must exceed current observed spend. +

+ ) : null} +
+ +
+ +
+ + + ); +} diff --git a/ui/src/components/BudgetPolicyCard.tsx b/ui/src/components/BudgetPolicyCard.tsx new file mode 100644 index 00000000..4ff72098 --- /dev/null +++ b/ui/src/components/BudgetPolicyCard.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from "react"; +import type { BudgetPolicySummary } from "@paperclipai/shared"; +import { AlertTriangle, PauseCircle, ShieldAlert, Wallet } from "lucide-react"; +import { cn, formatCents } from "../lib/utils"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; + +function centsInputValue(value: number) { + return (value / 100).toFixed(2); +} + +function parseDollarInput(value: string) { + const normalized = value.trim(); + if (normalized.length === 0) return 0; + const parsed = Number(normalized); + if (!Number.isFinite(parsed) || parsed < 0) return null; + return Math.round(parsed * 100); +} + +function windowLabel(windowKind: BudgetPolicySummary["windowKind"]) { + return windowKind === "lifetime" ? "Lifetime budget" : "Monthly UTC budget"; +} + +function statusTone(status: BudgetPolicySummary["status"]) { + if (status === "hard_stop") return "text-red-300 border-red-500/30 bg-red-500/10"; + if (status === "warning") return "text-amber-200 border-amber-500/30 bg-amber-500/10"; + return "text-emerald-200 border-emerald-500/30 bg-emerald-500/10"; +} + +export function BudgetPolicyCard({ + summary, + onSave, + isSaving, + compact = false, +}: { + summary: BudgetPolicySummary; + onSave?: (amountCents: number) => void; + isSaving?: boolean; + compact?: boolean; +}) { + const [draftBudget, setDraftBudget] = useState(centsInputValue(summary.amount)); + + useEffect(() => { + setDraftBudget(centsInputValue(summary.amount)); + }, [summary.amount]); + + const parsedDraft = parseDollarInput(draftBudget); + const canSave = typeof parsedDraft === "number" && parsedDraft !== summary.amount && Boolean(onSave); + const progress = summary.amount > 0 ? Math.min(100, summary.utilizationPercent) : 0; + const StatusIcon = summary.status === "hard_stop" ? ShieldAlert : summary.status === "warning" ? AlertTriangle : Wallet; + + return ( + + +
+
+
+ {summary.scopeType} +
+ {summary.scopeName} + {windowLabel(summary.windowKind)} +
+
+ + {summary.paused ? "Paused" : summary.status === "warning" ? "Warning" : summary.status === "hard_stop" ? "Hard stop" : "Healthy"} +
+
+
+ +
+
+
Observed
+
{formatCents(summary.observedAmount)}
+
+ {summary.amount > 0 ? `${summary.utilizationPercent}% of limit` : "No cap configured"} +
+
+
+
Budget
+
+ {summary.amount > 0 ? formatCents(summary.amount) : "Disabled"} +
+
+ Soft alert at {summary.warnPercent}%{summary.paused && summary.pauseReason ? ` · ${summary.pauseReason} pause` : ""} +
+
+
+ +
+
+ Remaining + {summary.amount > 0 ? formatCents(summary.remainingAmount) : "Unlimited"} +
+
+
+
+
+ + {summary.paused ? ( +
+ +
+ {summary.scopeType === "project" + ? "Execution is paused for this project until the budget is raised or the incident is dismissed." + : "Heartbeats are paused for this scope until the budget is raised or the incident is dismissed."} +
+
+ ) : null} + + {onSave ? ( +
+
+
+ + setDraftBudget(event.target.value)} + className="mt-2" + inputMode="decimal" + placeholder="0.00" + /> +
+ +
+ {parsedDraft === null ? ( +

Enter a valid non-negative dollar amount.

+ ) : null} +
+ ) : null} + + + ); +} diff --git a/ui/src/components/ClaudeSubscriptionPanel.tsx b/ui/src/components/ClaudeSubscriptionPanel.tsx new file mode 100644 index 00000000..3559a320 --- /dev/null +++ b/ui/src/components/ClaudeSubscriptionPanel.tsx @@ -0,0 +1,140 @@ +import type { QuotaWindow } from "@paperclipai/shared"; +import { cn, quotaSourceDisplayName } from "@/lib/utils"; + +interface ClaudeSubscriptionPanelProps { + windows: QuotaWindow[]; + source?: string | null; + error?: string | null; +} + +const WINDOW_ORDER = [ + "currentsession", + "currentweekallmodels", + "currentweeksonnetonly", + "currentweeksonnet", + "currentweekopusonly", + "currentweekopus", + "extrausage", +] as const; + +function normalizeLabel(text: string): string { + return text.toLowerCase().replace(/[^a-z0-9]+/g, ""); +} + +function detailText(window: QuotaWindow): string | null { + if (typeof window.detail === "string" && window.detail.trim().length > 0) return window.detail.trim(); + if (window.resetsAt) { + const formatted = new Date(window.resetsAt).toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + timeZoneName: "short", + }); + return `Resets ${formatted}`; + } + return null; +} + +function orderedWindows(windows: QuotaWindow[]): QuotaWindow[] { + return [...windows].sort((a, b) => { + const aIndex = WINDOW_ORDER.indexOf(normalizeLabel(a.label) as (typeof WINDOW_ORDER)[number]); + const bIndex = WINDOW_ORDER.indexOf(normalizeLabel(b.label) as (typeof WINDOW_ORDER)[number]); + return (aIndex === -1 ? WINDOW_ORDER.length : aIndex) - (bIndex === -1 ? WINDOW_ORDER.length : bIndex); + }); +} + +function fillClass(usedPercent: number | null): string { + if (usedPercent == null) return "bg-zinc-700"; + if (usedPercent >= 90) return "bg-red-400"; + if (usedPercent >= 70) return "bg-amber-400"; + return "bg-primary/70"; +} + +export function ClaudeSubscriptionPanel({ + windows, + source = null, + error = null, +}: ClaudeSubscriptionPanelProps) { + const ordered = orderedWindows(windows); + + return ( +
+
+
+
+ Anthropic subscription +
+
+ Live Claude quota windows. +
+
+ {source ? ( + + {quotaSourceDisplayName(source)} + + ) : null} +
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+ {ordered.map((window) => { + const normalized = normalizeLabel(window.label); + const detail = detailText(window); + if (normalized === "extrausage") { + return ( +
+
+
{window.label}
+ {window.valueLabel ? ( +
{window.valueLabel}
+ ) : null} +
+ {detail ? ( +
{detail}
+ ) : null} +
+ ); + } + + const width = Math.min(100, Math.max(0, window.usedPercent ?? 0)); + return ( +
+
+
+
{window.label}
+ {detail ? ( +
{detail}
+ ) : null} +
+ {window.usedPercent != null ? ( +
+ {window.usedPercent}% used +
+ ) : null} +
+ +
+
+
+
+ ); + })} +
+
+ ); +} diff --git a/ui/src/components/CodexSubscriptionPanel.tsx b/ui/src/components/CodexSubscriptionPanel.tsx new file mode 100644 index 00000000..db1ac2e4 --- /dev/null +++ b/ui/src/components/CodexSubscriptionPanel.tsx @@ -0,0 +1,157 @@ +import type { QuotaWindow } from "@paperclipai/shared"; +import { cn, quotaSourceDisplayName } from "@/lib/utils"; + +interface CodexSubscriptionPanelProps { + windows: QuotaWindow[]; + source?: string | null; + error?: string | null; +} + +const WINDOW_PRIORITY = [ + "5hlimit", + "weeklylimit", + "credits", +] as const; + +function normalizeLabel(text: string): string { + return text.toLowerCase().replace(/[^a-z0-9]+/g, ""); +} + +function orderedWindows(windows: QuotaWindow[]): QuotaWindow[] { + return [...windows].sort((a, b) => { + const aBase = normalizeLabel(a.label).replace(/^gpt53codexspark/, ""); + const bBase = normalizeLabel(b.label).replace(/^gpt53codexspark/, ""); + const aIndex = WINDOW_PRIORITY.indexOf(aBase as (typeof WINDOW_PRIORITY)[number]); + const bIndex = WINDOW_PRIORITY.indexOf(bBase as (typeof WINDOW_PRIORITY)[number]); + return (aIndex === -1 ? WINDOW_PRIORITY.length : aIndex) - (bIndex === -1 ? WINDOW_PRIORITY.length : bIndex); + }); +} + +function detailText(window: QuotaWindow): string | null { + if (typeof window.detail === "string" && window.detail.trim().length > 0) return window.detail.trim(); + if (!window.resetsAt) return null; + const formatted = new Date(window.resetsAt).toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + timeZoneName: "short", + }); + return `Resets ${formatted}`; +} + +function fillClass(usedPercent: number | null): string { + if (usedPercent == null) return "bg-zinc-700"; + if (usedPercent >= 90) return "bg-red-400"; + if (usedPercent >= 70) return "bg-amber-400"; + return "bg-primary/70"; +} + +function isModelSpecific(label: string): boolean { + const normalized = normalizeLabel(label); + return normalized.includes("gpt53codexspark") || normalized.includes("gpt5"); +} + +export function CodexSubscriptionPanel({ + windows, + source = null, + error = null, +}: CodexSubscriptionPanelProps) { + const ordered = orderedWindows(windows); + const accountWindows = ordered.filter((window) => !isModelSpecific(window.label)); + const modelWindows = ordered.filter((window) => isModelSpecific(window.label)); + + return ( +
+
+
+
+ Codex subscription +
+
+ Live Codex quota windows. +
+
+ {source ? ( + + {quotaSourceDisplayName(source)} + + ) : null} +
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+
+
+ Account windows +
+
+ {accountWindows.map((window) => ( + + ))} +
+
+ + {modelWindows.length > 0 ? ( +
+
+ Model windows +
+
+ {modelWindows.map((window) => ( + + ))} +
+
+ ) : null} +
+
+ ); +} + +function QuotaWindowRow({ window }: { window: QuotaWindow }) { + const detail = detailText(window); + if (window.usedPercent == null) { + return ( +
+
+
{window.label}
+ {window.valueLabel ? ( +
{window.valueLabel}
+ ) : null} +
+ {detail ? ( +
{detail}
+ ) : null} +
+ ); + } + + return ( +
+
+
+
{window.label}
+ {detail ? ( +
{detail}
+ ) : null} +
+
+ {window.usedPercent}% used +
+
+ +
+
+
+
+ ); +} diff --git a/ui/src/components/FinanceBillerCard.tsx b/ui/src/components/FinanceBillerCard.tsx new file mode 100644 index 00000000..542c1050 --- /dev/null +++ b/ui/src/components/FinanceBillerCard.tsx @@ -0,0 +1,44 @@ +import type { FinanceByBiller } from "@paperclipai/shared"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { formatCents, providerDisplayName } from "@/lib/utils"; + +interface FinanceBillerCardProps { + row: FinanceByBiller; +} + +export function FinanceBillerCard({ row }: FinanceBillerCardProps) { + return ( + + +
+
+ {providerDisplayName(row.biller)} + + {row.eventCount} event{row.eventCount === 1 ? "" : "s"} across {row.kindCount} kind{row.kindCount === 1 ? "" : "s"} + +
+
+
{formatCents(row.netCents)}
+
net
+
+
+
+ +
+
+
debits
+
{formatCents(row.debitCents)}
+
+
+
credits
+
{formatCents(row.creditCents)}
+
+
+
estimated
+
{formatCents(row.estimatedDebitCents)}
+
+
+
+
+ ); +} diff --git a/ui/src/components/FinanceKindCard.tsx b/ui/src/components/FinanceKindCard.tsx new file mode 100644 index 00000000..4b800a39 --- /dev/null +++ b/ui/src/components/FinanceKindCard.tsx @@ -0,0 +1,43 @@ +import type { FinanceByKind } from "@paperclipai/shared"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { financeEventKindDisplayName, formatCents } from "@/lib/utils"; + +interface FinanceKindCardProps { + rows: FinanceByKind[]; +} + +export function FinanceKindCard({ rows }: FinanceKindCardProps) { + return ( + + + Financial event mix + Account-level charges grouped by event kind. + + + {rows.length === 0 ? ( +

No finance events in this period.

+ ) : ( + rows.map((row) => ( +
+
+
{financeEventKindDisplayName(row.eventKind)}
+
+ {row.eventCount} event{row.eventCount === 1 ? "" : "s"} · {row.billerCount} biller{row.billerCount === 1 ? "" : "s"} +
+
+
+
{formatCents(row.netCents)}
+
+ {formatCents(row.debitCents)} debits +
+
+
+ )) + )} +
+
+ ); +} diff --git a/ui/src/components/FinanceTimelineCard.tsx b/ui/src/components/FinanceTimelineCard.tsx new file mode 100644 index 00000000..1fa516ba --- /dev/null +++ b/ui/src/components/FinanceTimelineCard.tsx @@ -0,0 +1,71 @@ +import type { FinanceEvent } from "@paperclipai/shared"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + financeDirectionDisplayName, + financeEventKindDisplayName, + formatCents, + formatDateTime, + providerDisplayName, +} from "@/lib/utils"; + +interface FinanceTimelineCardProps { + rows: FinanceEvent[]; + emptyMessage?: string; +} + +export function FinanceTimelineCard({ + rows, + emptyMessage = "No financial events in this period.", +}: FinanceTimelineCardProps) { + return ( + + + Recent financial events + Top-ups, fees, credits, commitments, and other non-request charges. + + + {rows.length === 0 ? ( +

{emptyMessage}

+ ) : ( + rows.map((row) => ( +
+
+
+
+ {financeEventKindDisplayName(row.eventKind)} + + {financeDirectionDisplayName(row.direction)} + + {formatDateTime(row.occurredAt)} +
+
+ {providerDisplayName(row.biller)} + {row.provider ? ` -> ${providerDisplayName(row.provider)}` : ""} + {row.model ? {row.model} : null} +
+ {(row.description || row.externalInvoiceId || row.region || row.pricingTier) && ( +
+ {row.description ?
{row.description}
: null} + {row.externalInvoiceId ?
invoice {row.externalInvoiceId}
: null} + {row.region ?
region {row.region}
: null} + {row.pricingTier ?
tier {row.pricingTier}
: null} +
+ )} +
+
+
{formatCents(row.amountCents)}
+
{row.currency}
+ {row.estimated ?
estimated
: null} +
+
+
+ )) + )} +
+
+ ); +} diff --git a/ui/src/components/ProviderQuotaCard.tsx b/ui/src/components/ProviderQuotaCard.tsx index 29861cba..21800bc1 100644 --- a/ui/src/components/ProviderQuotaCard.tsx +++ b/ui/src/components/ProviderQuotaCard.tsx @@ -1,8 +1,17 @@ import { useMemo } from "react"; import type { CostByProviderModel, CostWindowSpendRow, QuotaWindow } from "@paperclipai/shared"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; import { QuotaBar } from "./QuotaBar"; -import { formatCents, formatTokens, providerDisplayName } from "@/lib/utils"; +import { ClaudeSubscriptionPanel } from "./ClaudeSubscriptionPanel"; +import { CodexSubscriptionPanel } from "./CodexSubscriptionPanel"; +import { + billingTypeDisplayName, + formatCents, + formatTokens, + providerDisplayName, + quotaSourceDisplayName, +} from "@/lib/utils"; // ordered display labels for rolling-window rows const ROLLING_WINDOWS = ["5h", "24h", "7d"] as const; @@ -21,6 +30,9 @@ interface ProviderQuotaCardProps { showDeficitNotch: boolean; /** live subscription quota windows from the provider's own api */ quotaWindows?: QuotaWindow[]; + quotaError?: string | null; + quotaSource?: string | null; + quotaLoading?: boolean; } export function ProviderQuotaCard({ @@ -32,6 +44,9 @@ export function ProviderQuotaCard({ windowRows, showDeficitNotch, quotaWindows = [], + quotaError = null, + quotaSource = null, + quotaLoading = false, }: ProviderQuotaCardProps) { // single-pass aggregation over rows — memoized so the 8 derived values are not // recomputed on every parent render tick (providers tab polls every 30s, and each @@ -108,6 +123,11 @@ export function ProviderQuotaCard({ () => Math.max(...windowRows.map((r) => r.costCents), 0), [windowRows], ); + const isClaudeQuotaPanel = provider === "anthropic"; + const isCodexQuotaPanel = provider === "openai" && quotaSource?.startsWith("codex-"); + const supportsSubscriptionQuota = provider === "anthropic" || provider === "openai"; + const showSubscriptionQuotaSection = + supportsSubscriptionQuota && (quotaLoading || quotaWindows.length > 0 || quotaError != null); return ( @@ -183,7 +203,7 @@ export function ProviderQuotaCard({ {formatCents(cents)}
-
+
)} - {/* subscription quota windows from provider api — shown when data is available */} - {quotaWindows.length > 0 && ( - <> -
-
-

- Subscription quota -

-
- {quotaWindows.map((qw) => { - const fillColor = - qw.usedPercent == null - ? null - : qw.usedPercent >= 90 - ? "bg-red-400" - : qw.usedPercent >= 70 - ? "bg-yellow-400" - : "bg-green-400"; - return ( -
-
- {qw.label} - - {qw.valueLabel != null ? ( - {qw.valueLabel} - ) : qw.usedPercent != null ? ( - {qw.usedPercent}% used - ) : null} -
- {qw.usedPercent != null && fillColor != null && ( -
-
-
- )} - {qw.resetsAt && ( -

- resets {new Date(qw.resetsAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })} -

- )} -
- ); - })} -
-
- - )} - {/* subscription usage — shown when any subscription-billed runs exist */} {totalSubRuns > 0 && ( <> @@ -258,6 +228,12 @@ export function ProviderQuotaCard({

{totalSubRuns} runs {" · "} + {totalSubTokens > 0 && ( + <> + {formatTokens(totalSubTokens)} total + {" · "} + + )} {formatTokens(totalSubInputTokens)} in {" · "} {formatTokens(totalSubOutputTokens)} out @@ -292,9 +268,14 @@ export function ProviderQuotaCard({

{/* model name and cost */}
- - {row.model} - +
+ + {row.model} + + + {providerDisplayName(row.biller)} · {billingTypeDisplayName(row.billingType)} + +
{formatTokens(rowTokens)} tok @@ -303,7 +284,7 @@ export function ProviderQuotaCard({
{/* token share bar */} -
+
)} + + {/* subscription quota windows from provider api — shown when data is available */} + {showSubscriptionQuotaSection && ( + <> +
+
+
+

+ Subscription quota +

+ {quotaSource && !isClaudeQuotaPanel && !isCodexQuotaPanel ? ( + + {quotaSourceDisplayName(quotaSource)} + + ) : null} +
+ {quotaLoading ? ( + + ) : isClaudeQuotaPanel ? ( + + ) : isCodexQuotaPanel ? ( + + ) : ( + <> + {quotaError ? ( +

+ {quotaError} +

+ ) : null} +
+ {quotaWindows.map((qw) => { + const fillColor = + qw.usedPercent == null + ? null + : qw.usedPercent >= 90 + ? "bg-red-400" + : qw.usedPercent >= 70 + ? "bg-yellow-400" + : "bg-green-400"; + return ( +
+
+ {qw.label} + + {qw.valueLabel != null ? ( + {qw.valueLabel} + ) : qw.usedPercent != null ? ( + {qw.usedPercent}% used + ) : null} +
+ {qw.usedPercent != null && fillColor != null && ( +
+
+
+ )} + {qw.detail ? ( +

+ {qw.detail} +

+ ) : qw.resetsAt ? ( +

+ resets {new Date(qw.resetsAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })} +

+ ) : null} +
+ ); + })} +
+ + )} +
+ + )} ); } + +function QuotaPanelSkeleton() { + return ( +
+
+
+ + +
+ +
+
+ {Array.from({ length: 3 }).map((_, index) => ( +
+
+
+ + +
+ +
+ +
+ ))} +
+
+ ); +} diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index a8480828..8bf8b089 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -164,6 +164,12 @@ const dashboard: DashboardSummary = { monthUtilizationPercent: 90, }, pendingApprovals: 1, + budgets: { + activeIncidents: 0, + pendingApprovals: 0, + pausedAgents: 0, + pausedProjects: 0, + }, }; describe("inbox helpers", () => { diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index 3ae44c70..28d9b4e9 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -43,6 +43,9 @@ export const queryKeys = { list: (companyId: string) => ["goals", companyId] as const, detail: (id: string) => ["goals", "detail", id] as const, }, + budgets: { + overview: (companyId: string) => ["budgets", "overview", companyId] as const, + }, approvals: { list: (companyId: string, status?: string) => ["approvals", companyId, status] as const, @@ -73,6 +76,16 @@ export const queryKeys = { ["costs", companyId, from, to] as const, usageByProvider: (companyId: string, from?: string, to?: string) => ["usage-by-provider", companyId, from, to] as const, + usageByBiller: (companyId: string, from?: string, to?: string) => + ["usage-by-biller", companyId, from, to] as const, + financeSummary: (companyId: string, from?: string, to?: string) => + ["finance-summary", companyId, from, to] as const, + financeByBiller: (companyId: string, from?: string, to?: string) => + ["finance-by-biller", companyId, from, to] as const, + financeByKind: (companyId: string, from?: string, to?: string) => + ["finance-by-kind", companyId, from, to] as const, + financeEvents: (companyId: string, from?: string, to?: string, limit: number = 100) => + ["finance-events", companyId, from, to, limit] as const, usageWindowSpend: (companyId: string) => ["usage-window-spend", companyId] as const, usageQuotaWindows: (companyId: string) => diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index f25b46f8..dcab46c1 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -1,6 +1,7 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import { deriveAgentUrlKey, deriveProjectUrlKey } from "@paperclipai/shared"; +import type { BillingType, FinanceDirection, FinanceEventKind } from "@paperclipai/shared"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -53,6 +54,8 @@ export function providerDisplayName(provider: string): string { const map: Record = { anthropic: "Anthropic", openai: "OpenAI", + openrouter: "OpenRouter", + chatgpt: "ChatGPT", google: "Google", cursor: "Cursor", jetbrains: "JetBrains AI", @@ -60,6 +63,84 @@ export function providerDisplayName(provider: string): string { return map[provider.toLowerCase()] ?? provider; } +export function billingTypeDisplayName(billingType: BillingType): string { + const map: Record = { + metered_api: "Metered API", + subscription_included: "Subscription", + subscription_overage: "Subscription overage", + credits: "Credits", + fixed: "Fixed", + unknown: "Unknown", + }; + return map[billingType]; +} + +export function quotaSourceDisplayName(source: string): string { + const map: Record = { + "anthropic-oauth": "Anthropic OAuth", + "claude-cli": "Claude CLI", + "codex-rpc": "Codex app server", + "codex-wham": "ChatGPT WHAM", + }; + return map[source] ?? source; +} + +function coerceBillingType(value: unknown): BillingType | null { + if ( + value === "metered_api" || + value === "subscription_included" || + value === "subscription_overage" || + value === "credits" || + value === "fixed" || + value === "unknown" + ) { + return value; + } + return null; +} + +function readRunCostUsd(payload: Record | null): number { + if (!payload) return 0; + for (const key of ["costUsd", "cost_usd", "total_cost_usd"] as const) { + const value = payload[key]; + if (typeof value === "number" && Number.isFinite(value)) return value; + } + return 0; +} + +export function visibleRunCostUsd( + usage: Record | null, + result: Record | null = null, +): number { + const billingType = coerceBillingType(usage?.billingType) ?? coerceBillingType(result?.billingType); + if (billingType === "subscription_included") return 0; + return readRunCostUsd(usage) || readRunCostUsd(result); +} + +export function financeEventKindDisplayName(eventKind: FinanceEventKind): string { + const map: Record = { + inference_charge: "Inference charge", + platform_fee: "Platform fee", + credit_purchase: "Credit purchase", + credit_refund: "Credit refund", + credit_expiry: "Credit expiry", + byok_fee: "BYOK fee", + gateway_overhead: "Gateway overhead", + log_storage_charge: "Log storage", + logpush_charge: "Logpush", + provisioned_capacity_charge: "Provisioned capacity", + training_charge: "Training", + custom_model_import_charge: "Custom model import", + custom_model_storage_charge: "Custom model storage", + manual_adjustment: "Manual adjustment", + }; + return map[eventKind]; +} + +export function financeDirectionDisplayName(direction: FinanceDirection): string { + return direction === "credit" ? "Credit" : "Debit"; +} + /** Build an issue URL using the human-readable identifier when available. */ export function issueUrl(issue: { id: string; identifier?: string | null }): string { return `/issues/${issue.identifier ?? issue.id}`; diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 30921807..c1c9ebfb 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents"; +import { budgetsApi } from "../api/budgets"; import { heartbeatsApi } from "../api/heartbeats"; import { ApiError } from "../api/client"; import { ChartCard, RunActivityChart, PriorityChart, IssueStatusChart, SuccessRateChart } from "../components/ActivityCharts"; @@ -24,8 +25,9 @@ import { CopyText } from "../components/CopyText"; import { EntityRow } from "../components/EntityRow"; import { Identity } from "../components/Identity"; import { PageSkeleton } from "../components/PageSkeleton"; +import { BudgetPolicyCard } from "../components/BudgetPolicyCard"; import { ScrollToBottom } from "../components/ScrollToBottom"; -import { formatCents, formatDate, relativeTime, formatTokens } from "../lib/utils"; +import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; import { Tabs } from "@/components/ui/tabs"; @@ -58,7 +60,15 @@ import { import { Input } from "@/components/ui/input"; import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker"; import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView"; -import { isUuidLike, type Agent, type HeartbeatRun, type HeartbeatRunEvent, type AgentRuntimeState, type LiveEvent } from "@paperclipai/shared"; +import { + isUuidLike, + type Agent, + type BudgetPolicySummary, + type HeartbeatRun, + type HeartbeatRunEvent, + type AgentRuntimeState, + type LiveEvent, +} from "@paperclipai/shared"; import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; import { agentRouteRef } from "../lib/utils"; @@ -204,8 +214,7 @@ function runMetrics(run: HeartbeatRun) { "cache_read_input_tokens", ); const cost = - usageNumber(usage, "costUsd", "cost_usd", "total_cost_usd") || - usageNumber(result, "total_cost_usd", "cost_usd", "costUsd"); + visibleRunCostUsd(usage, result); return { input, output, @@ -294,11 +303,50 @@ export function AgentDetail() { enabled: !!resolvedCompanyId, }); + const { data: budgetOverview } = useQuery({ + queryKey: queryKeys.budgets.overview(resolvedCompanyId ?? "__none__"), + queryFn: () => budgetsApi.overview(resolvedCompanyId!), + enabled: !!resolvedCompanyId, + refetchInterval: 30_000, + staleTime: 5_000, + }); + const assignedIssues = (allIssues ?? []) .filter((i) => i.assigneeAgentId === agent?.id) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo); const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agent?.id && a.status !== "terminated"); + const agentBudgetSummary = useMemo(() => { + const matched = budgetOverview?.policies.find( + (policy) => policy.scopeType === "agent" && policy.scopeId === (agent?.id ?? routeAgentRef), + ); + if (matched) return matched; + const budgetMonthlyCents = agent?.budgetMonthlyCents ?? 0; + const spentMonthlyCents = agent?.spentMonthlyCents ?? 0; + return { + policyId: "", + companyId: resolvedCompanyId ?? "", + scopeType: "agent", + scopeId: agent?.id ?? routeAgentRef, + scopeName: agent?.name ?? "Agent", + metric: "billed_cents", + windowKind: "calendar_month_utc", + amount: budgetMonthlyCents, + observedAmount: spentMonthlyCents, + remainingAmount: Math.max(0, budgetMonthlyCents - spentMonthlyCents), + utilizationPercent: + budgetMonthlyCents > 0 ? Number(((spentMonthlyCents / budgetMonthlyCents) * 100).toFixed(2)) : 0, + warnPercent: 80, + hardStopEnabled: true, + notifyEnabled: true, + isActive: budgetMonthlyCents > 0, + status: budgetMonthlyCents > 0 && spentMonthlyCents >= budgetMonthlyCents ? "hard_stop" : "ok", + paused: agent?.status === "paused", + pauseReason: agent?.pauseReason ?? null, + windowStart: new Date(), + windowEnd: new Date(), + } satisfies BudgetPolicySummary; + }, [agent, budgetOverview?.policies, resolvedCompanyId, routeAgentRef]); const mobileLiveRun = useMemo( () => (heartbeats ?? []).find((r) => r.status === "running" || r.status === "queued") ?? null, [heartbeats], @@ -360,6 +408,24 @@ export function AgentDetail() { }, }); + const budgetMutation = useMutation({ + mutationFn: (amount: number) => + budgetsApi.upsertPolicy(resolvedCompanyId!, { + scopeType: "agent", + scopeId: agent?.id ?? routeAgentRef, + amount, + windowKind: "calendar_month_utc", + }), + onSuccess: () => { + if (!resolvedCompanyId) return; + queryClient.invalidateQueries({ queryKey: queryKeys.budgets.overview(resolvedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); + queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); + queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(resolvedCompanyId) }); + }, + }); + const updateIcon = useMutation({ mutationFn: (icon: string) => agentsApi.update(agentLookupRef, { icon }, resolvedCompanyId ?? undefined), onSuccess: () => { @@ -579,6 +645,15 @@ export function AgentDetail() { )} + {!urlRunId && resolvedCompanyId ? ( + budgetMutation.mutate(amount)} + /> + ) : null} + {actionError &&

{actionError}

} {isPendingApproval && (

@@ -849,8 +924,8 @@ function CostsSection({ }) { const runsWithCost = runs .filter((r) => { - const u = r.usageJson as Record | null; - return u && (u.cost_usd || u.total_cost_usd || u.input_tokens); + const metrics = runMetrics(r); + return metrics.cost > 0 || metrics.input > 0 || metrics.output > 0 || metrics.cached > 0; }) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); @@ -892,16 +967,16 @@ function CostsSection({ {runsWithCost.slice(0, 10).map((run) => { - const u = run.usageJson as Record; + const metrics = runMetrics(run); return ( {formatDate(run.createdAt)} {run.id.slice(0, 8)} - {formatTokens(Number(u.input_tokens ?? 0))} - {formatTokens(Number(u.output_tokens ?? 0))} + {formatTokens(metrics.input)} + {formatTokens(metrics.output)} - {(u.cost_usd || u.total_cost_usd) - ? `$${Number(u.cost_usd ?? u.total_cost_usd ?? 0).toFixed(4)}` + {metrics.cost > 0 + ? `$${metrics.cost.toFixed(4)}` : "-" } diff --git a/ui/src/pages/ApprovalDetail.tsx b/ui/src/pages/ApprovalDetail.tsx index 9c962c09..741f9657 100644 --- a/ui/src/pages/ApprovalDetail.tsx +++ b/ui/src/pages/ApprovalDetail.tsx @@ -147,6 +147,7 @@ export function ApprovalDetail() { const payload = approval.payload as Record; const linkedAgentId = typeof payload.agentId === "string" ? payload.agentId : null; const isActionable = approval.status === "pending" || approval.status === "revision_requested"; + const isBudgetApproval = approval.type === "budget_override_required"; const TypeIcon = typeIcon[approval.type] ?? defaultTypeIcon; const showApprovedBanner = searchParams.get("resolved") === "approved" && approval.status === "approved"; const primaryLinkedIssue = linkedIssues?.[0] ?? null; @@ -260,7 +261,7 @@ export function ApprovalDetail() {

)}
- {isActionable && ( + {isActionable && !isBudgetApproval && ( <> - ))} - {preset === "custom" && ( -
- setCustomFrom(e.target.value)} - className="h-8 border border-input bg-background px-2 text-sm text-foreground" +
+
+
+

Costs

+

+ Inference spend, platform fees, credits, and live quota windows. +

+
+ +
+ {PRESET_KEYS.map((key) => ( + + ))} +
+
+ + {preset === "custom" ? ( +
+ setCustomFrom(event.target.value)} + className="h-9 rounded-md border border-input bg-background px-3 text-sm text-foreground" + /> + to + setCustomTo(event.target.value)} + className="h-9 rounded-md border border-input bg-background px-3 text-sm text-foreground" + /> +
+ ) : null} + +
+ - to - setCustomTo(e.target.value)} - className="h-8 border border-input bg-background px-2 text-sm text-foreground" + 0 ? String(activeBudgetIncidents.length) : ( + spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 + ? `${spendData.summary.utilizationPercent}%` + : "Open" + )} + subtitle={ + activeBudgetIncidents.length > 0 + ? `${budgetData?.pausedAgentCount ?? 0} agents paused · ${budgetData?.pausedProjectCount ?? 0} projects paused` + : spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 + ? `${formatCents(spendData.summary.spendCents)} of ${formatCents(spendData.summary.budgetCents)}` + : "No monthly cap configured" + } + icon={Coins} + /> + +
- )}
- {/* main spend / providers tab switcher */} - setMainTab(v as "spend" | "providers")}> - - Spend + setMainTab(value as typeof mainTab)}> + + Overview + Budgets Providers + Billers + Finance - {/* ── spend tab ─────────────────────────────────────────────── */} - - {spendLoading ? ( - - ) : preset === "custom" && !customReady ? ( + + {showCustomPrompt ? (

Select a start and end date to load data.

- ) : spendError ? ( -

{(spendError as Error).message}

- ) : spendData ? ( + ) : showOverviewLoading ? ( + + ) : overviewError ? ( +

{(overviewError as Error).message}

+ ) : ( <> - {/* summary card */} - - -
-

{PRESET_LABELS[preset]}

- {spendData.summary.budgetCents > 0 && ( -

- {spendData.summary.utilizationPercent}% utilized -

- )} -
-

- {formatCents(spendData.summary.spendCents)}{" "} - - {spendData.summary.budgetCents > 0 - ? `/ ${formatCents(spendData.summary.budgetCents)}` - : "Unlimited budget"} - -

- {spendData.summary.budgetCents > 0 && ( -
-
90 - ? "bg-red-400" - : spendData.summary.utilizationPercent > 70 - ? "bg-yellow-400" - : "bg-green-400" - }`} - style={{ width: `${Math.min(100, spendData.summary.utilizationPercent)}%` }} - /> + {activeBudgetIncidents.length > 0 ? ( +
+ {activeBudgetIncidents.slice(0, 2).map((incident) => ( + incidentMutation.mutate({ incidentId: incident.id, action: "keep_paused" })} + onRaiseAndResume={(amount) => + incidentMutation.mutate({ + incidentId: incident.id, + action: "raise_budget_and_resume", + amount, + })} + /> + ))} +
+ ) : null} + +
+ + + Inference ledger + + Request-scoped inference spend for the selected period. + + + +
+
+
+ {formatCents(spendData?.summary.spendCents ?? 0)} +
+
+ {spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 + ? `Budget ${formatCents(spendData.summary.budgetCents)}` + : "Unlimited budget"} +
+
+
+
usage
+
+ {formatTokens(inferenceTokenTotal)} +
+
- )} + {spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 ? ( +
+
+
90 + ? "bg-red-400" + : spendData.summary.utilizationPercent > 70 + ? "bg-yellow-400" + : "bg-emerald-400", + )} + style={{ width: `${Math.min(100, spendData.summary.utilizationPercent)}%` }} + /> +
+
+ {spendData.summary.utilizationPercent}% of monthly budget consumed in this range. +
+
+ ) : null} + + + + +
+ +
+ + + By agent + What each agent consumed in the selected period. + + + {(spendData?.byAgent.length ?? 0) === 0 ? ( +

No cost events yet.

+ ) : ( + spendData?.byAgent.map((row) => { + const modelRows = agentModelRows.get(row.agentId) ?? []; + const isExpanded = expandedAgents.has(row.agentId); + const hasBreakdown = modelRows.length > 0; + return ( +
+
hasBreakdown && toggleAgent(row.agentId)} + > +
+ {hasBreakdown ? ( + isExpanded + ? + : + ) : ( + + )} + + {row.agentStatus === "terminated" ? : null} +
+
+
{formatCents(row.costCents)}
+
+ in {formatTokens(row.inputTokens + row.cachedInputTokens)} · out {formatTokens(row.outputTokens)} +
+ {(row.apiRunCount > 0 || row.subscriptionRunCount > 0) ? ( +
+ {row.apiRunCount > 0 ? `${row.apiRunCount} api` : "0 api"} + {" · "} + {row.subscriptionRunCount > 0 + ? `${row.subscriptionRunCount} subscription` + : "0 subscription"} +
+ ) : null} +
+
+ + {isExpanded && modelRows.length > 0 ? ( +
+ {modelRows.map((modelRow) => { + const sharePct = row.costCents > 0 ? Math.round((modelRow.costCents / row.costCents) * 100) : 0; + return ( +
+
+
+ {providerDisplayName(modelRow.provider)} + / + {modelRow.model} +
+
+ {providerDisplayName(modelRow.biller)} · {billingTypeDisplayName(modelRow.billingType)} +
+
+
+
+ {formatCents(modelRow.costCents)} + ({sharePct}%) +
+
+ {formatTokens(modelRow.inputTokens + modelRow.cachedInputTokens + modelRow.outputTokens)} tok +
+
+
+ ); + })} +
+ ) : null} +
+ ); + }) + )} +
+
+ +
+ + + By project + Run costs attributed through project-linked issues. + + + {(spendData?.byProject.length ?? 0) === 0 ? ( +

No project-attributed run costs yet.

+ ) : ( + spendData?.byProject.map((row, index) => ( +
+ {row.projectName ?? row.projectId ?? "Unattributed"} + {formatCents(row.costCents)} +
+ )) + )} +
+
+ + +
+
+ + )} + + + + {budgetLoading ? ( + + ) : budgetError ? ( +

{(budgetError as Error).message}

+ ) : ( + <> + + + Budget control plane + + Hard-stop spend limits for agents and projects. Provider subscription quota stays separate and appears under Providers. + + + + + + + - {/* by agent / by project */} -
- - -

By Agent

- {spendData.byAgent.length === 0 ? ( -

No cost events yet.

- ) : ( -
- {spendData.byAgent.map((row) => { - const modelRows = agentModelRows.get(row.agentId) ?? []; - const isExpanded = expandedAgents.has(row.agentId); - const hasBreakdown = modelRows.length > 0; - return ( -
-
hasBreakdown && toggleAgent(row.agentId)} - > -
- {hasBreakdown ? ( - isExpanded - ? - : - ) : ( - - )} - - {row.agentStatus === "terminated" && ( - - )} -
-
- {formatCents(row.costCents)} - - in {formatTokens(row.inputTokens)} / out {formatTokens(row.outputTokens)} tok - - {(row.apiRunCount > 0 || row.subscriptionRunCount > 0) && ( - - {row.apiRunCount > 0 ? `api runs: ${row.apiRunCount}` : null} - {row.apiRunCount > 0 && row.subscriptionRunCount > 0 ? " | " : null} - {row.subscriptionRunCount > 0 - ? `subscription runs: ${row.subscriptionRunCount} (${formatTokens(row.subscriptionInputTokens)} in / ${formatTokens(row.subscriptionOutputTokens)} out tok)` - : null} - - )} -
-
- {isExpanded && modelRows.length > 0 && ( -
- {modelRows.map((m) => { - const totalAgentCents = row.costCents; - const sharePct = totalAgentCents > 0 - ? Math.round((m.costCents / totalAgentCents) * 100) - : 0; - return ( -
-
- {providerDisplayName(m.provider)} - / - {m.model} -
-
- - {formatCents(m.costCents)} - ({sharePct}%) - - - in {formatTokens(m.inputTokens)} / out {formatTokens(m.outputTokens)} tok - -
-
- ); - })} -
- )} -
- ); - })} -
- )} -
-
- - - -

By Project

- {spendData.byProject.length === 0 ? ( -

No project-attributed run costs yet.

- ) : ( -
- {spendData.byProject.map((row, i) => ( -
- - {row.projectName ?? row.projectId ?? "Unattributed"} - - {formatCents(row.costCents)} -
- ))} -
- )} -
-
-
- - ) : null} -
- - {/* ── providers tab ─────────────────────────────────────────── */} - - {preset === "custom" && !customReady ? ( -

Select a start and end date to load data.

- ) : ( - - - - - {providers.length === 0 ? ( -

No cost events in this period.

- ) : ( -
- {providers.map((p) => ( - 0 ? ( +
+
+

Active incidents

+

+ Resolve hard stops here by raising the budget or explicitly keeping the scope paused. +

+
+
+ {activeBudgetIncidents.map((incident) => ( + incidentMutation.mutate({ incidentId: incident.id, action: "keep_paused" })} + onRaiseAndResume={(amount) => + incidentMutation.mutate({ + incidentId: incident.id, + action: "raise_budget_and_resume", + amount, + })} /> ))}
- )} - +
+ ) : null} - {providers.map((p) => ( - - +
+ {(["company", "agent", "project"] as const).map((scopeType) => { + const rows = budgetPoliciesByScope[scopeType]; + if (rows.length === 0) return null; + return ( +
+
+

{scopeType} budgets

+

+ {scopeType === "company" + ? "Company-wide monthly policy." + : scopeType === "agent" + ? "Recurring monthly spend policies for individual agents." + : "Lifetime spend policies for execution-bound projects."} +

+
+
+ {rows.map((summary) => ( + + policyMutation.mutate({ + scopeType: summary.scopeType, + scopeId: summary.scopeId, + amount, + windowKind: summary.windowKind, + })} + /> + ))} +
+
+ ); + })} + + {budgetPolicies.length === 0 ? ( + + + No budget policies yet. Set agent and project budgets from their detail pages, or use the existing company monthly budget control. + + + ) : null} +
+ + )} +
+ + + {showCustomPrompt ? ( +

Select a start and end date to load data.

+ ) : ( + <> + + + + + {providers.length === 0 ? ( +

No cost events in this period.

+ ) : ( +
+ {providers.map((provider) => ( + + ))} +
+ )}
- ))} -
+ + {providers.map((provider) => ( + + + + ))} + + + )} +
+ + + {showCustomPrompt ? ( +

Select a start and end date to load data.

+ ) : ( + <> + + + + + {billers.length === 0 ? ( +

No billable events in this period.

+ ) : ( +
+ {billers.map((biller) => { + const row = (byBiller.get(biller) ?? [])[0]; + if (!row) return null; + const providerRows = (providerData ?? []).filter((entry) => entry.biller === biller); + return ( + + ); + })} +
+ )} +
+ + {billers.map((biller) => { + const row = (byBiller.get(biller) ?? [])[0]; + if (!row) return null; + const providerRows = (providerData ?? []).filter((entry) => entry.biller === biller); + return ( + + + + ); + })} +
+ + )} +
+ + + {showCustomPrompt ? ( +

Select a start and end date to load data.

+ ) : financeLoading ? ( + + ) : financeError ? ( +

{(financeError as Error).message}

+ ) : ( + <> + + +
+
+ + + By biller + Account-level financial events grouped by who charged or credited them. + + + {(financeData?.byBiller.length ?? 0) === 0 ? ( +

No finance events yet.

+ ) : ( + financeData?.byBiller.map((row) => ) + )} +
+
+ +
+ + +
+ )}
diff --git a/ui/src/pages/Dashboard.tsx b/ui/src/pages/Dashboard.tsx index 46589760..45613380 100644 --- a/ui/src/pages/Dashboard.tsx +++ b/ui/src/pages/Dashboard.tsx @@ -19,7 +19,7 @@ import { ActivityRow } from "../components/ActivityRow"; import { Identity } from "../components/Identity"; import { timeAgo } from "../lib/timeAgo"; import { cn, formatCents } from "../lib/utils"; -import { Bot, CircleDot, DollarSign, ShieldCheck, LayoutDashboard } from "lucide-react"; +import { Bot, CircleDot, DollarSign, ShieldCheck, LayoutDashboard, PauseCircle } from "lucide-react"; import { ActiveAgentsPanel } from "../components/ActiveAgentsPanel"; import { ChartCard, RunActivityChart, PriorityChart, IssueStatusChart, SuccessRateChart } from "../components/ActivityCharts"; import { PageSkeleton } from "../components/PageSkeleton"; @@ -210,6 +210,25 @@ export function Dashboard() { {data && ( <> + {data.budgets.activeIncidents > 0 ? ( +
+
+ +
+

+ {data.budgets.activeIncidents} active budget incident{data.budgets.activeIncidents === 1 ? "" : "s"} +

+

+ {data.budgets.pausedAgents} agents paused · {data.budgets.pausedProjects} projects paused · {data.budgets.pendingApprovals} pending budget approvals +

+
+
+ + Open budgets + +
+ ) : null} +
- Awaiting board review + {data.budgets.pendingApprovals > 0 + ? `${data.budgets.pendingApprovals} budget overrides awaiting board review` + : "Awaiting board review"} } /> diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 53b52a74..1cfa7431 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -13,7 +13,7 @@ import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { readIssueDetailBreadcrumb } from "../lib/issueDetailBreadcrumb"; import { useProjectOrder } from "../hooks/useProjectOrder"; -import { relativeTime, cn, formatTokens } from "../lib/utils"; +import { relativeTime, cn, formatTokens, visibleRunCostUsd } from "../lib/utils"; import { InlineEditor } from "../components/InlineEditor"; import { CommentThread } from "../components/CommentThread"; import { IssueDocumentsSection } from "../components/IssueDocumentsSection"; @@ -417,9 +417,7 @@ export function IssueDetail() { "cached_input_tokens", "cache_read_input_tokens", ); - const runCost = - usageNumber(usage, "costUsd", "cost_usd", "total_cost_usd") || - usageNumber(result, "total_cost_usd", "cost_usd", "costUsd"); + const runCost = visibleRunCostUsd(usage, result); if (runCost > 0) hasCost = true; if (runInput + runOutput + runCached > 0) hasTokens = true; input += runInput; diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index 5134c22b..b74eb9d6 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -1,7 +1,8 @@ import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { useParams, useNavigate, useLocation, Navigate } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { PROJECT_COLORS, isUuidLike } from "@paperclipai/shared"; +import { PROJECT_COLORS, isUuidLike, type BudgetPolicySummary } from "@paperclipai/shared"; +import { budgetsApi } from "../api/budgets"; import { projectsApi } from "../api/projects"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; @@ -14,6 +15,7 @@ import { queryKeys } from "../lib/queryKeys"; import { ProjectProperties, type ProjectConfigFieldKey, type ProjectFieldSaveState } from "../components/ProjectProperties"; import { InlineEditor } from "../components/InlineEditor"; import { StatusBadge } from "../components/StatusBadge"; +import { BudgetPolicyCard } from "../components/BudgetPolicyCard"; import { IssuesList } from "../components/IssuesList"; import { PageSkeleton } from "../components/PageSkeleton"; import { PageTabBar } from "../components/PageTabBar"; @@ -296,6 +298,14 @@ export function ProjectDetail() { }, }); + const { data: budgetOverview } = useQuery({ + queryKey: queryKeys.budgets.overview(resolvedCompanyId ?? "__none__"), + queryFn: () => budgetsApi.overview(resolvedCompanyId!), + enabled: !!resolvedCompanyId, + refetchInterval: 30_000, + staleTime: 5_000, + }); + useEffect(() => { setBreadcrumbs([ { label: "Projects", href: "/projects" }, @@ -377,6 +387,53 @@ export function ProjectDetail() { } }, [invalidateProject, lookupCompanyId, projectLookupRef, resolvedCompanyId, scheduleFieldReset, setFieldState]); + const projectBudgetSummary = useMemo(() => { + const matched = budgetOverview?.policies.find( + (policy) => policy.scopeType === "project" && policy.scopeId === (project?.id ?? routeProjectRef), + ); + if (matched) return matched; + return { + policyId: "", + companyId: resolvedCompanyId ?? "", + scopeType: "project", + scopeId: project?.id ?? routeProjectRef, + scopeName: project?.name ?? "Project", + metric: "billed_cents", + windowKind: "lifetime", + amount: 0, + observedAmount: 0, + remainingAmount: 0, + utilizationPercent: 0, + warnPercent: 80, + hardStopEnabled: true, + notifyEnabled: true, + isActive: false, + status: "ok", + paused: Boolean(project?.pausedAt), + pauseReason: project?.pauseReason ?? null, + windowStart: new Date(), + windowEnd: new Date(), + } satisfies BudgetPolicySummary; + }, [budgetOverview?.policies, project, resolvedCompanyId, routeProjectRef]); + + const budgetMutation = useMutation({ + mutationFn: (amount: number) => + budgetsApi.upsertPolicy(resolvedCompanyId!, { + scopeType: "project", + scopeId: project?.id ?? routeProjectRef, + amount, + windowKind: "lifetime", + }), + onSuccess: () => { + if (!resolvedCompanyId) return; + queryClient.invalidateQueries({ queryKey: queryKeys.budgets.overview(resolvedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(routeProjectRef) }); + queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(projectLookupRef) }); + queryClient.invalidateQueries({ queryKey: queryKeys.projects.list(resolvedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(resolvedCompanyId) }); + }, + }); + if (pluginTabFromSearch && !pluginDetailSlotsLoading && !activePluginTab) { return ; } @@ -469,6 +526,15 @@ export function ProjectDetail() { /> + {resolvedCompanyId ? ( + budgetMutation.mutate(amount)} + /> + ) : null} + {activeTab === "overview" && (