diff --git a/.gitignore b/.gitignore index 1f867cca..f2c9b9a7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ tmp/ .vscode/ .claude/settings.local.json .paperclip-local/ +/.idea/ +/.agents/ # Doc maintenance cursor .doc-review-cursor @@ -44,4 +46,4 @@ tmp/ # Playwright tests/e2e/test-results/ tests/e2e/playwright-report/ -.superset/ \ No newline at end of file +.superset/ diff --git a/cli/src/__tests__/company-delete.test.ts b/cli/src/__tests__/company-delete.test.ts index 6858a3d1..73d5e440 100644 --- a/cli/src/__tests__/company-delete.test.ts +++ b/cli/src/__tests__/company-delete.test.ts @@ -8,12 +8,16 @@ function makeCompany(overrides: Partial): Company { name: "Alpha", description: null, status: "active", + pauseReason: null, + pausedAt: null, issuePrefix: "ALP", issueCounter: 1, budgetMonthlyCents: 0, spentMonthlyCents: 0, requireBoardApprovalForNewAgents: false, brandColor: null, + logoAssetId: null, + logoUrl: null, createdAt: new Date(), updatedAt: new Date(), ...overrides, 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/docs/api/companies.md b/docs/api/companies.md index a0aafae5..00e7ab66 100644 --- a/docs/api/companies.md +++ b/docs/api/companies.md @@ -38,10 +38,33 @@ PATCH /api/companies/{companyId} { "name": "Updated Name", "description": "Updated description", - "budgetMonthlyCents": 100000 + "budgetMonthlyCents": 100000, + "logoAssetId": "b9f5e911-6de5-4cd0-8dc6-a55a13bc02f6" } ``` +## Upload Company Logo + +Upload an image for a company icon and store it as that company’s logo. + +``` +POST /api/companies/{companyId}/logo +Content-Type: multipart/form-data +``` + +Valid image content types: + +- `image/png` +- `image/jpeg` +- `image/jpg` +- `image/webp` +- `image/gif` +- `image/svg+xml` + +Company logo uploads use the normal Paperclip attachment size limit. + +Then set the company logo by PATCHing the returned `assetId` into `logoAssetId`. + ## Archive Company ``` @@ -58,6 +81,8 @@ Archives a company. Archived companies are hidden from default listings. | `name` | string | Company name | | `description` | string | Company description | | `status` | string | `active`, `paused`, `archived` | +| `logoAssetId` | string | Optional asset id for the stored logo image | +| `logoUrl` | string | Optional Paperclip asset content path for the stored logo image | | `budgetMonthlyCents` | number | Monthly budget limit | | `createdAt` | string | ISO timestamp | | `updatedAt` | string | ISO timestamp | 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 c3dab36f..62a24e41 100644 --- a/packages/adapter-utils/src/index.ts +++ b/packages/adapter-utils/src/index.ts @@ -22,6 +22,8 @@ export type { HireApprovedPayload, HireApprovedHookResult, ServerAdapterModule, + QuotaWindow, + ProviderQuotaResult, TranscriptEntry, StdoutLineParser, CLIAdapterModule, @@ -33,3 +35,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 26195804..3fccd1d4 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; @@ -209,6 +218,37 @@ export interface HireApprovedHookResult { detail?: Record; } +// --------------------------------------------------------------------------- +// Quota window types — used by adapters that can report provider quota/rate-limit state +// --------------------------------------------------------------------------- + +/** a single rate-limit or usage window returned by a provider quota API */ +export interface QuotaWindow { + /** human label, e.g. "5h", "7d", "Sonnet 7d", "Credits" */ + label: string; + /** percent of the window already consumed (0-100), null when not reported */ + usedPercent: number | null; + /** iso timestamp when this window resets, null when not reported */ + 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 */ + error?: string; + windows: QuotaWindow[]; +} + export interface ServerAdapterModule { type: string; execute(ctx: AdapterExecutionContext): Promise; @@ -228,6 +268,12 @@ export interface ServerAdapterModule { payload: HireApprovedPayload, adapterConfig: Record, ) => Promise; + /** + * Optional: fetch live provider quota/rate-limit windows for this adapter. + * Returns a ProviderQuotaResult so the server can aggregate across adapters + * without knowing provider-specific credential paths or API shapes. + */ + getQuotaWindows?: () => Promise; } // --------------------------------------------------------------------------- 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 eb7d89e3..6b174dd2 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -333,7 +333,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise typeof entry[1] === "string", + ), + ); + const billingType = resolveClaudeBillingType(effectiveEnv); const skillsDir = await buildSkillsDir(config); // When instructionsFilePath is configured, create a combined temp file that @@ -540,6 +545,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) return fromEnv.trim(); + return path.join(os.homedir(), ".claude"); +} + +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"); + } catch { + return null; + } + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return null; + } + if (typeof parsed !== "object" || parsed === null) return null; + const obj = parsed as Record; + const oauth = obj["claudeAiOauth"]; + if (typeof oauth !== "object" || oauth === null) return null; + const token = (oauth as Record)["accessToken"]; + 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; + return Math.min(100, Math.round(utilization * 100)); +} + +/** fetch with an abort-based timeout so a hanging provider api doesn't block the response indefinitely */ +export async function fetchWithTimeout(url: string, init: RequestInit, ms = 8000): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), ms); + try { + return await fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + +export async function fetchClaudeQuota(token: string): Promise { + const resp = await fetchWithTimeout("https://api.anthropic.com/api/oauth/usage", { + headers: { + Authorization: `Bearer ${token}`, + "anthropic-beta": "oauth-2025-04-20", + }, + }); + if (!resp.ok) throw new Error(`anthropic usage api returned ${resp.status}`); + const body = (await resp.json()) as AnthropicUsageResponse; + const windows: QuotaWindow[] = []; + + if (body.five_hour != null) { + windows.push({ + 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: "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: "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: "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; +} + +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 72972fa2..6a305f46 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, @@ -63,6 +63,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")), @@ -330,8 +336,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); @@ -523,6 +534,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) return fromEnv.trim(); + return path.join(os.homedir(), ".codex"); +} + +interface CodexLegacyAuthFile { + accessToken?: string | null; + accountId?: string | 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 { + raw = await fs.readFile(authPath, "utf8"); + } catch { + return null; + } + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return null; + } + if (typeof parsed !== "object" || parsed === null) 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 = + 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 | number | null; +} + +interface WhamCredits { + balance?: number | null; + unlimited?: boolean | null; +} + +interface WhamUsageResponse { + plan_type?: string | null; + rate_limit?: { + primary_window?: WhamWindow | null; + secondary_window?: WhamWindow | null; + } | null; + credits?: WhamCredits | null; +} + +/** + * Map a window duration in seconds to a human-readable label. + * Falls back to the provided fallback string when seconds is null/undefined. + */ +export function secondsToWindowLabel( + seconds: number | null | undefined, + fallback: string, +): string { + if (seconds == null) return fallback; + const hours = seconds / 3600; + if (hours < 6) return "5h"; + if (hours <= 24) return "24h"; + if (hours <= 168) return "7d"; + return `${Math.round(hours / 24)}d`; +} + +/** fetch with an abort-based timeout so a hanging provider api doesn't block the response indefinitely */ +export async function fetchWithTimeout( + url: string, + init: RequestInit, + ms = 8000, +): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), ms); + try { + return await fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + +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, +): Promise { + const headers: Record = { + Authorization: `Bearer ${token}`, + }; + if (accountId) headers["ChatGPT-Account-Id"] = accountId; + + const resp = await fetchWithTimeout("https://chatgpt.com/backend-api/wham/usage", { headers }); + if (!resp.ok) throw new Error(`chatgpt wham api returned ${resp.status}`); + const body = (await resp.json()) as WhamUsageResponse; + const windows: QuotaWindow[] = []; + + const rateLimit = body.rate_limit; + if (rateLimit?.primary_window != null) { + const w = rateLimit.primary_window; + windows.push({ + 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; + windows.push({ + 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) { + const balance = body.credits.balance; + const valueLabel = balance != null ? `$${(balance / 100).toFixed(2)} remaining` : "N/A"; + windows.push({ + label: "Credits", + usedPercent: null, + resetsAt: null, + valueLabel, + detail: null, + }); + } + return 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 daca09bc..02afe4ff 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, @@ -48,6 +48,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; @@ -248,8 +259,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); @@ -479,6 +495,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); @@ -426,6 +431,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"); } @@ -368,6 +372,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise entry.name)); const selectedEntries = skillsEntries.filter((entry) => desiredSet.has(entry.name)); if (selectedEntries.length === 0) return; - const piSkillsHome = path.join(os.homedir(), ".pi", "agent", "skills"); await fs.mkdir(piSkillsHome, { recursive: true }); const removedSkills = await removeMaintainerOnlySkillSymlinks( @@ -92,6 +91,10 @@ async function ensurePiSkillsInjected( } } +function resolvePiBiller(env: Record, provider: string | null): string { + return inferOpenAiCompatibleBiller(env, null) ?? provider ?? "unknown"; +} + async function ensureSessionsDir(): Promise { await fs.mkdir(PAPERCLIP_SESSIONS_DIR, { recursive: true }); return PAPERCLIP_SESSIONS_DIR; @@ -455,6 +458,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/0030_rich_magneto.sql b/packages/db/src/migrations/0030_rich_magneto.sql new file mode 100644 index 00000000..76d44de7 --- /dev/null +++ b/packages/db/src/migrations/0030_rich_magneto.sql @@ -0,0 +1,12 @@ +CREATE TABLE "company_logos" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "asset_id" uuid NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "company_logos" ADD CONSTRAINT "company_logos_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "company_logos" ADD CONSTRAINT "company_logos_asset_id_assets_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "company_logos_company_uq" ON "company_logos" USING btree ("company_id");--> statement-breakpoint +CREATE UNIQUE INDEX "company_logos_asset_uq" ON "company_logos" USING btree ("asset_id"); \ No newline at end of file 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/0033_shiny_black_tarantula.sql b/packages/db/src/migrations/0033_shiny_black_tarantula.sql new file mode 100644 index 00000000..79665982 --- /dev/null +++ b/packages/db/src/migrations/0033_shiny_black_tarantula.sql @@ -0,0 +1,2 @@ +ALTER TABLE "companies" ADD COLUMN "pause_reason" text;--> statement-breakpoint +ALTER TABLE "companies" ADD COLUMN "paused_at" timestamp with time zone; diff --git a/packages/db/src/migrations/0034_fat_dormammu.sql b/packages/db/src/migrations/0034_fat_dormammu.sql new file mode 100644 index 00000000..0a3b300c --- /dev/null +++ b/packages/db/src/migrations/0034_fat_dormammu.sql @@ -0,0 +1,2 @@ +DROP INDEX "budget_incidents_policy_window_threshold_idx";--> statement-breakpoint +CREATE UNIQUE INDEX "budget_incidents_policy_window_threshold_idx" ON "budget_incidents" USING btree ("policy_id","window_start","threshold_type") WHERE "budget_incidents"."status" <> 'dismissed'; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0030_snapshot.json b/packages/db/src/migrations/meta/0030_snapshot.json index 66f99c20..4f21ce46 100644 --- a/packages/db/src/migrations/meta/0030_snapshot.json +++ b/packages/db/src/migrations/meta/0030_snapshot.json @@ -1,5 +1,5 @@ { - "id": "5c4c1d61-6416-4280-8262-3035cd5e92a7", + "id": "ff007d90-e1a0-4df3-beab-a5be4a47273c", "prevId": "fdb36f4e-6463-497d-b704-22d33be9b450", "version": "7", "dialect": "postgresql", @@ -2179,6 +2179,110 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.company_logos": { + "name": "company_logos", + "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 + }, + "asset_id": { + "name": "asset_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": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.company_memberships": { "name": "company_memberships", "schema": "", @@ -2657,173 +2761,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.company_skills": { - "name": "company_skills", - "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 - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "markdown": { - "name": "markdown", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_type": { - "name": "source_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'local_path'" - }, - "source_locator": { - "name": "source_locator", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "source_ref": { - "name": "source_ref", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "trust_level": { - "name": "trust_level", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'markdown_only'" - }, - "compatibility": { - "name": "compatibility", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'compatible'" - }, - "file_inventory": { - "name": "file_inventory", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "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": { - "company_skills_company_slug_idx": { - "name": "company_skills_company_slug_idx", - "columns": [ - { - "expression": "company_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "company_skills_company_name_idx": { - "name": "company_skills_company_name_idx", - "columns": [ - { - "expression": "company_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "company_skills_company_id_companies_id_fk": { - "name": "company_skills_company_id_companies_id_fk", - "tableFrom": "company_skills", - "tableTo": "companies", - "columnsFrom": [ - "company_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.cost_events": { "name": "cost_events", "schema": "", diff --git a/packages/db/src/migrations/meta/0026_snapshot.json b/packages/db/src/migrations/meta/0031_snapshot.json similarity index 85% rename from packages/db/src/migrations/meta/0026_snapshot.json rename to packages/db/src/migrations/meta/0031_snapshot.json index a3ebaad7..aa6b78b2 100644 --- a/packages/db/src/migrations/meta/0026_snapshot.json +++ b/packages/db/src/migrations/meta/0031_snapshot.json @@ -1,6 +1,6 @@ { - "id": "5f8dd541-9e28-4a42-890b-fc4a301604ac", - "prevId": "bd8d9b8d-3012-4c58-bcfd-b3215c164f82", + "id": "67faba5f-0106-4163-81f6-cd0d90488574", + "prevId": "ff007d90-e1a0-4df3-beab-a5be4a47273c", "version": "7", "dialect": "postgresql", "tables": { @@ -2698,6 +2698,12 @@ "primaryKey": false, "notNull": false }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "billing_code": { "name": "billing_code", "type": "text", @@ -2710,6 +2716,20 @@ "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", @@ -2723,6 +2743,13 @@ "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", @@ -2798,6 +2825,81 @@ "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": { @@ -2865,6 +2967,773 @@ ], "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": {}, @@ -4204,6 +5073,174 @@ "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": "", @@ -4632,6 +5669,12 @@ "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", @@ -5749,6 +6792,12 @@ "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", @@ -6190,4 +7239,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} 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/0033_snapshot.json b/packages/db/src/migrations/meta/0033_snapshot.json new file mode 100644 index 00000000..54d6fa53 --- /dev/null +++ b/packages/db/src/migrations/meta/0033_snapshot.json @@ -0,0 +1,9038 @@ +{ + "id": "4382d430-5951-42e5-9533-402302bfc223", + "prevId": "fd2770d1-d831-4d2a-989b-ad4bf92e575e", + "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'" + }, + "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 + }, + "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.plugin_company_settings": { + "name": "plugin_company_settings", + "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 + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_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": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_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": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "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 + }, + "public.company_logos": { + "name": "company_logos", + "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 + }, + "asset_id": { + "name": "asset_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": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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/0034_snapshot.json b/packages/db/src/migrations/meta/0034_snapshot.json new file mode 100644 index 00000000..6f6bd524 --- /dev/null +++ b/packages/db/src/migrations/meta/0034_snapshot.json @@ -0,0 +1,9039 @@ +{ + "id": "53b16771-42c3-41c7-af85-0bfbb9f9013b", + "prevId": "4382d430-5951-42e5-9533-402302bfc223", + "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, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "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'" + }, + "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 + }, + "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_logos": { + "name": "company_logos", + "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 + }, + "asset_id": { + "name": "asset_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": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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.plugin_company_settings": { + "name": "plugin_company_settings", + "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 + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_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": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_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": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "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": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 11a6bee1..8ad03efe 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -215,8 +215,36 @@ { "idx": 30, "version": "7", - "when": 1773542603948, - "tag": "0030_friendly_loners", + "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 + }, + { + "idx": 33, + "version": "7", + "when": 1773664961967, + "tag": "0033_shiny_black_tarantula", + "breakpoints": true + }, + { + "idx": 34, + "version": "7", + "when": 1773697572188, + "tag": "0034_fat_dormammu", "breakpoints": true } ] 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..57b9b3b0 --- /dev/null +++ b/packages/db/src/schema/budget_incidents.ts @@ -0,0 +1,42 @@ +import { sql } from "drizzle-orm"; +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, + ).where(sql`${table.status} <> 'dismissed'`), + }), +); 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/companies.ts b/packages/db/src/schema/companies.ts index 29c82b71..83d6e193 100644 --- a/packages/db/src/schema/companies.ts +++ b/packages/db/src/schema/companies.ts @@ -7,6 +7,8 @@ export const companies = pgTable( name: text("name").notNull(), description: text("description"), status: text("status").notNull().default("active"), + pauseReason: text("pause_reason"), + pausedAt: timestamp("paused_at", { withTimezone: true }), issuePrefix: text("issue_prefix").notNull().default("PAP"), issueCounter: integer("issue_counter").notNull().default(0), budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0), diff --git a/packages/db/src/schema/company_logos.ts b/packages/db/src/schema/company_logos.ts new file mode 100644 index 00000000..13e0abe0 --- /dev/null +++ b/packages/db/src/schema/company_logos.ts @@ -0,0 +1,18 @@ +import { pgTable, uuid, timestamp, uniqueIndex } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { assets } from "./assets.js"; + +export const companyLogos = pgTable( + "company_logos", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }), + assetId: uuid("asset_id").notNull().references(() => assets.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyUq: uniqueIndex("company_logos_company_uq").on(table.companyId), + assetUq: uniqueIndex("company_logos_asset_uq").on(table.assetId), + }), +); 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 47196238..b542320e 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -1,4 +1,5 @@ export { companies } from "./companies.js"; +export { companyLogos } from "./company_logos.js"; export { authUsers, authSessions, authAccounts, authVerifications } from "./auth.js"; export { instanceUserRoles } from "./instance_user_roles.js"; export { agents } from "./agents.js"; @@ -6,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"; @@ -30,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 898d2d9c..6bd12791 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, @@ -149,9 +171,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, @@ -213,6 +250,8 @@ export type { PluginJobRecord, PluginJobRunRecord, PluginWebhookDeliveryRecord, + QuotaWindow, + ProviderQuotaResult, } from "./types/index.js"; export { @@ -279,11 +318,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, @@ -299,6 +342,7 @@ export { type RotateSecret, type UpdateSecret, createCostEventSchema, + createFinanceEventSchema, updateBudgetSchema, createAssetImageMetadataSchema, createCompanyInviteSchema, @@ -309,6 +353,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/company.ts b/packages/shared/src/types/company.ts index 435be80d..9f6d3168 100644 --- a/packages/shared/src/types/company.ts +++ b/packages/shared/src/types/company.ts @@ -1,16 +1,20 @@ -import type { CompanyStatus } from "../constants.js"; +import type { CompanyStatus, PauseReason } from "../constants.js"; export interface Company { id: string; name: string; description: string | null; status: CompanyStatus; + pauseReason: PauseReason | null; + pausedAt: Date | null; issuePrefix: string; issueCounter: number; budgetMonthlyCents: number; spentMonthlyCents: number; requireBoardApprovalForNewAgents: boolean; brandColor: string | null; + logoAssetId: string | null; + logoUrl: string | null; createdAt: Date; updatedAt: Date; } diff --git a/packages/shared/src/types/cost.ts b/packages/shared/src/types/cost.ts index c5b2bb2e..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,9 +34,80 @@ 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; +} + +/** cost attributed to a project via heartbeat run → activity log → issue → project chain */ +export interface CostByProject { + projectId: string | null; + 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 79e9876e..0472fee6 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -60,6 +60,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, @@ -70,7 +78,8 @@ export type { CompanySecret, SecretProviderDescriptor, } from "./secrets.js"; -export type { CostEvent, CostSummary, CostByAgent } 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, @@ -90,6 +99,7 @@ export type { JoinRequest, InstanceUserRoleGrant, } from "./access.js"; +export type { QuotaWindow, ProviderQuotaResult } from "./quota.js"; export type { CompanyPortabilityInclude, CompanyPortabilityEnvInput, 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 new file mode 100644 index 00000000..56b68bb5 --- /dev/null +++ b/packages/shared/src/types/quota.ts @@ -0,0 +1,26 @@ +/** a single rate-limit or usage window returned by a provider quota API */ +export interface QuotaWindow { + /** human label, e.g. "5h", "7d", "Sonnet 7d", "Credits" */ + label: string; + /** percent of the window already consumed (0-100), null when not reported */ + usedPercent: number | null; + /** iso timestamp when this window resets, null when not reported */ + 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 */ + error?: string; + windows: QuotaWindow[]; +} 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/company.ts b/packages/shared/src/validators/company.ts index 407d2ae4..bb4851f4 100644 --- a/packages/shared/src/validators/company.ts +++ b/packages/shared/src/validators/company.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { COMPANY_STATUSES } from "../constants.js"; +const logoAssetIdSchema = z.string().uuid().nullable().optional(); + export const createCompanySchema = z.object({ name: z.string().min(1), description: z.string().optional().nullable(), @@ -16,6 +18,7 @@ export const updateCompanySchema = createCompanySchema spentMonthlyCents: z.number().int().nonnegative().optional(), requireBoardApprovalForNewAgents: z.boolean().optional(), brandColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(), + logoAssetId: logoAssetIdSchema, }); export type UpdateCompany = 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 914cc4e7..8009204b 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, @@ -149,6 +156,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..391ddb44 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -34,6 +34,11 @@ const env = { PAPERCLIP_UI_DEV_MIDDLEWARE: "true", }; +if (mode === "watch") { + env.PAPERCLIP_MIGRATION_PROMPT ??= "never"; + env.PAPERCLIP_MIGRATION_AUTO_APPLY ??= "true"; +} + if (tailscaleAuth) { env.PAPERCLIP_DEPLOYMENT_MODE = "authenticated"; env.PAPERCLIP_DEPLOYMENT_EXPOSURE = "private"; @@ -46,6 +51,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 @@ -89,14 +118,17 @@ async function runPnpm(args, options = {}) { async function maybePreflightMigrations() { if (mode !== "watch") return; - if (process.env.PAPERCLIP_MIGRATION_PROMPT === "never") return; const status = await runPnpm( ["--filter", "@paperclipai/db", "exec", "tsx", "src/migration-status.ts", "--json"], { 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,15 +136,19 @@ 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) { return; } - const autoApply = process.env.PAPERCLIP_MIGRATION_AUTO_APPLY === "true"; + const autoApply = env.PAPERCLIP_MIGRATION_AUTO_APPLY === "true"; let shouldApply = autoApply; if (!autoApply) { @@ -135,7 +171,13 @@ async function maybePreflightMigrations() { } } - if (!shouldApply) return; + if (!shouldApply) { + process.stderr.write( + `[paperclip] Pending migrations detected (${formatPendingMigrationSummary(payload.pendingMigrations)}). ` + + "Refusing to start watch mode against a stale schema.\n", + ); + process.exit(1); + } const migrate = spawn(pnpmBin, ["db:migrate"], { stdio: "inherit", @@ -174,10 +216,6 @@ async function buildPluginSdk() { await buildPluginSdk(); -if (mode === "watch") { - env.PAPERCLIP_MIGRATION_PROMPT = "never"; -} - const serverScript = mode === "watch" ? "dev:watch" : "dev"; const child = spawn( pnpmBin, diff --git a/server/package.json b/server/package.json index 1887d64c..c5cc23f7 100644 --- a/server/package.json +++ b/server/package.json @@ -23,7 +23,7 @@ ], "scripts": { "dev": "tsx src/index.ts", - "dev:watch": "cross-env PAPERCLIP_MIGRATION_PROMPT=never tsx watch --ignore ../ui/node_modules --ignore ../ui/.vite --ignore ../ui/dist src/index.ts", + "dev:watch": "cross-env PAPERCLIP_MIGRATION_PROMPT=never PAPERCLIP_MIGRATION_AUTO_APPLY=true tsx watch --ignore ../ui/node_modules --ignore ../ui/.vite --ignore ../ui/dist src/index.ts", "prepare:ui-dist": "bash ../scripts/prepare-server-ui-dist.sh", "build": "tsc", "prepack": "pnpm run prepare:ui-dist", @@ -51,10 +51,12 @@ "better-auth": "1.4.18", "chokidar": "^4.0.3", "detect-port": "^2.1.0", + "dompurify": "^3.3.2", "dotenv": "^17.0.1", "drizzle-orm": "^0.38.4", "embedded-postgres": "^18.1.0-beta.16", "express": "^5.1.0", + "jsdom": "^28.1.0", "multer": "^2.0.2", "open": "^11.0.0", "pino": "^9.6.0", @@ -66,6 +68,7 @@ "devDependencies": { "@types/express": "^5.0.0", "@types/express-serve-static-core": "^5.0.0", + "@types/jsdom": "^28.0.0", "@types/multer": "^2.0.0", "@types/node": "^24.6.0", "@types/supertest": "^6.0.2", diff --git a/server/src/__tests__/assets.test.ts b/server/src/__tests__/assets.test.ts new file mode 100644 index 00000000..b7bec332 --- /dev/null +++ b/server/src/__tests__/assets.test.ts @@ -0,0 +1,250 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import express from "express"; +import request from "supertest"; +import { MAX_ATTACHMENT_BYTES } from "../attachment-types.js"; +import { assetRoutes } from "../routes/assets.js"; +import type { StorageService } from "../storage/types.js"; + +const { createAssetMock, getAssetByIdMock, logActivityMock } = vi.hoisted(() => ({ + createAssetMock: vi.fn(), + getAssetByIdMock: vi.fn(), + logActivityMock: vi.fn(), +})); + +vi.mock("../services/index.js", () => ({ + assetService: vi.fn(() => ({ + create: createAssetMock, + getById: getAssetByIdMock, + })), + logActivity: logActivityMock, +})); + +function createAsset() { + const now = new Date("2026-01-01T00:00:00.000Z"); + return { + id: "asset-1", + companyId: "company-1", + provider: "local", + objectKey: "assets/abc", + contentType: "image/png", + byteSize: 40, + sha256: "sha256-sample", + originalFilename: "logo.png", + createdByAgentId: null, + createdByUserId: "user-1", + createdAt: now, + updatedAt: now, + }; +} + +function createStorageService(contentType = "image/png"): StorageService { + const putFile: StorageService["putFile"] = vi.fn(async (input: { + companyId: string; + namespace: string; + originalFilename: string | null; + contentType: string; + body: Buffer; + }) => { + return { + provider: "local_disk" as const, + objectKey: `${input.namespace}/${input.originalFilename ?? "upload"}`, + contentType: contentType || input.contentType, + byteSize: input.body.length, + sha256: "sha256-sample", + originalFilename: input.originalFilename, + }; + }); + + return { + provider: "local_disk" as const, + putFile, + getObject: vi.fn(), + headObject: vi.fn(), + deleteObject: vi.fn(), + }; +} + +function createApp(storage: ReturnType) { + const app = express(); + app.use((req, _res, next) => { + req.actor = { + type: "board", + source: "local_implicit", + userId: "user-1", + }; + next(); + }); + app.use("/api", assetRoutes({} as any, storage)); + return app; +} + +describe("POST /api/companies/:companyId/assets/images", () => { + afterEach(() => { + createAssetMock.mockReset(); + getAssetByIdMock.mockReset(); + logActivityMock.mockReset(); + }); + + it("accepts PNG image uploads and returns an asset path", async () => { + const png = createStorageService("image/png"); + const app = createApp(png); + + createAssetMock.mockResolvedValue(createAsset()); + + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "goals") + .attach("file", Buffer.from("png"), "logo.png"); + + expect(res.status).toBe(201); + expect(res.body.contentPath).toBe("/api/assets/asset-1/content"); + expect(createAssetMock).toHaveBeenCalledTimes(1); + expect(png.putFile).toHaveBeenCalledWith({ + companyId: "company-1", + namespace: "assets/goals", + originalFilename: "logo.png", + contentType: "image/png", + body: expect.any(Buffer), + }); + }); + + it("allows supported non-image attachments outside the company logo flow", async () => { + const text = createStorageService("text/plain"); + const app = createApp(text); + + createAssetMock.mockResolvedValue({ + ...createAsset(), + contentType: "text/plain", + originalFilename: "note.txt", + }); + + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "issues/drafts") + .attach("file", Buffer.from("hello"), { filename: "note.txt", contentType: "text/plain" }); + + expect(res.status).toBe(201); + expect(text.putFile).toHaveBeenCalledWith({ + companyId: "company-1", + namespace: "assets/issues/drafts", + originalFilename: "note.txt", + contentType: "text/plain", + body: expect.any(Buffer), + }); + }); +}); + +describe("POST /api/companies/:companyId/logo", () => { + afterEach(() => { + createAssetMock.mockReset(); + getAssetByIdMock.mockReset(); + logActivityMock.mockReset(); + }); + + it("accepts PNG logo uploads and returns an asset path", async () => { + const png = createStorageService("image/png"); + const app = createApp(png); + + createAssetMock.mockResolvedValue(createAsset()); + + const res = await request(app) + .post("/api/companies/company-1/logo") + .attach("file", Buffer.from("png"), "logo.png"); + + expect(res.status).toBe(201); + expect(res.body.contentPath).toBe("/api/assets/asset-1/content"); + expect(createAssetMock).toHaveBeenCalledTimes(1); + expect(png.putFile).toHaveBeenCalledWith({ + companyId: "company-1", + namespace: "assets/companies", + originalFilename: "logo.png", + contentType: "image/png", + body: expect.any(Buffer), + }); + }); + + it("sanitizes SVG logo uploads before storing them", async () => { + const svg = createStorageService("image/svg+xml"); + const app = createApp(svg); + + createAssetMock.mockResolvedValue({ + ...createAsset(), + contentType: "image/svg+xml", + originalFilename: "logo.svg", + }); + + const res = await request(app) + .post("/api/companies/company-1/logo") + .attach( + "file", + Buffer.from( + "", + ), + "logo.svg", + ); + + expect(res.status).toBe(201); + expect(svg.putFile).toHaveBeenCalledTimes(1); + const stored = (svg.putFile as ReturnType).mock.calls[0]?.[0]; + expect(stored.contentType).toBe("image/svg+xml"); + expect(stored.originalFilename).toBe("logo.svg"); + const body = stored.body.toString("utf8"); + expect(body).toContain(" { + const png = createStorageService("image/png"); + const app = createApp(png); + createAssetMock.mockResolvedValue(createAsset()); + + const file = Buffer.alloc(150 * 1024, "a"); + const res = await request(app) + .post("/api/companies/company-1/logo") + .attach("file", file, "within-limit.png"); + + expect(res.status).toBe(201); + }); + + it("rejects logo files larger than the general attachment limit", async () => { + const app = createApp(createStorageService()); + createAssetMock.mockResolvedValue(createAsset()); + + const file = Buffer.alloc(MAX_ATTACHMENT_BYTES + 1, "a"); + const res = await request(app) + .post("/api/companies/company-1/logo") + .attach("file", file, "too-large.png"); + + expect(res.status).toBe(422); + expect(res.body.error).toBe(`Image exceeds ${MAX_ATTACHMENT_BYTES} bytes`); + }); + + it("rejects unsupported image types", async () => { + const app = createApp(createStorageService("text/plain")); + createAssetMock.mockResolvedValue(createAsset()); + + const res = await request(app) + .post("/api/companies/company-1/logo") + .attach("file", Buffer.from("not an image"), "note.txt"); + + expect(res.status).toBe(422); + expect(res.body.error).toBe("Unsupported image type: text/plain"); + expect(createAssetMock).not.toHaveBeenCalled(); + }); + + it("rejects SVG image uploads that cannot be sanitized", async () => { + const app = createApp(createStorageService("image/svg+xml")); + createAssetMock.mockResolvedValue(createAsset()); + + const res = await request(app) + .post("/api/companies/company-1/logo") + .attach("file", Buffer.from("not actually svg"), "logo.svg"); + + expect(res.status).toBe(422); + expect(res.body.error).toBe("SVG could not be sanitized"); + expect(createAssetMock).not.toHaveBeenCalled(); + }); +}); diff --git a/server/src/__tests__/budgets-service.test.ts b/server/src/__tests__/budgets-service.test.ts new file mode 100644 index 00000000..b85050c5 --- /dev/null +++ b/server/src/__tests__/budgets-service.test.ts @@ -0,0 +1,311 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { budgetService } from "../services/budgets.ts"; + +const mockLogActivity = vi.hoisted(() => vi.fn()); + +vi.mock("../services/activity-log.js", () => ({ + logActivity: mockLogActivity, +})); + +type SelectResult = unknown[]; + +function createDbStub(selectResults: SelectResult[]) { + const pendingSelects = [...selectResults]; + const selectWhere = vi.fn(async () => pendingSelects.shift() ?? []); + const selectThen = vi.fn((resolve: (value: unknown[]) => unknown) => Promise.resolve(resolve(pendingSelects.shift() ?? []))); + const selectOrderBy = vi.fn(async () => pendingSelects.shift() ?? []); + const selectFrom = vi.fn(() => ({ + where: selectWhere, + then: selectThen, + orderBy: selectOrderBy, + })); + const select = vi.fn(() => ({ + from: selectFrom, + })); + + const insertValues = vi.fn(); + const insertReturning = vi.fn(async () => pendingInserts.shift() ?? []); + const insert = vi.fn(() => ({ + values: insertValues.mockImplementation(() => ({ + returning: insertReturning, + })), + })); + + const updateSet = vi.fn(); + const updateWhere = vi.fn(async () => pendingUpdates.shift() ?? []); + const update = vi.fn(() => ({ + set: updateSet.mockImplementation(() => ({ + where: updateWhere, + })), + })); + + const pendingInserts: unknown[][] = []; + const pendingUpdates: unknown[][] = []; + + return { + db: { + select, + insert, + update, + }, + queueInsert: (rows: unknown[]) => { + pendingInserts.push(rows); + }, + queueUpdate: (rows: unknown[] = []) => { + pendingUpdates.push(rows); + }, + selectWhere, + insertValues, + updateSet, + }; +} + +describe("budgetService", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("creates a hard-stop incident and pauses an agent when spend exceeds a budget", async () => { + const policy = { + id: "policy-1", + companyId: "company-1", + scopeType: "agent", + scopeId: "agent-1", + metric: "billed_cents", + windowKind: "calendar_month_utc", + amount: 100, + warnPercent: 80, + hardStopEnabled: true, + notifyEnabled: false, + isActive: true, + }; + + const dbStub = createDbStub([ + [policy], + [{ total: 150 }], + [], + [{ + companyId: "company-1", + name: "Budget Agent", + status: "running", + pauseReason: null, + }], + ]); + + dbStub.queueInsert([{ + id: "approval-1", + companyId: "company-1", + status: "pending", + }]); + dbStub.queueInsert([{ + id: "incident-1", + companyId: "company-1", + policyId: "policy-1", + approvalId: "approval-1", + }]); + dbStub.queueUpdate([]); + const cancelWorkForScope = vi.fn().mockResolvedValue(undefined); + + const service = budgetService(dbStub.db as any, { cancelWorkForScope }); + await service.evaluateCostEvent({ + companyId: "company-1", + agentId: "agent-1", + projectId: null, + } as any); + + expect(dbStub.insertValues).toHaveBeenCalledWith( + expect.objectContaining({ + companyId: "company-1", + type: "budget_override_required", + status: "pending", + }), + ); + expect(dbStub.insertValues).toHaveBeenCalledWith( + expect.objectContaining({ + companyId: "company-1", + policyId: "policy-1", + thresholdType: "hard", + amountLimit: 100, + amountObserved: 150, + approvalId: "approval-1", + }), + ); + expect(dbStub.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ + status: "paused", + pauseReason: "budget", + pausedAt: expect.any(Date), + }), + ); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "budget.hard_threshold_crossed", + entityId: "incident-1", + }), + ); + expect(cancelWorkForScope).toHaveBeenCalledWith({ + companyId: "company-1", + scopeType: "agent", + scopeId: "agent-1", + }); + }); + + it("blocks new work when an agent hard-stop remains exceeded even if the agent is not paused yet", async () => { + const agentPolicy = { + id: "policy-agent-1", + companyId: "company-1", + scopeType: "agent", + scopeId: "agent-1", + metric: "billed_cents", + windowKind: "calendar_month_utc", + amount: 100, + warnPercent: 80, + hardStopEnabled: true, + notifyEnabled: true, + isActive: true, + }; + + const dbStub = createDbStub([ + [{ + status: "running", + pauseReason: null, + companyId: "company-1", + name: "Budget Agent", + }], + [{ + status: "active", + name: "Paperclip", + }], + [], + [agentPolicy], + [{ total: 120 }], + ]); + + const service = budgetService(dbStub.db as any); + const block = await service.getInvocationBlock("company-1", "agent-1"); + + expect(block).toEqual({ + scopeType: "agent", + scopeId: "agent-1", + scopeName: "Budget Agent", + reason: "Agent cannot start because its budget hard-stop is still exceeded.", + }); + }); + + it("surfaces a budget-owned company pause distinctly from a manual pause", async () => { + const dbStub = createDbStub([ + [{ + status: "idle", + pauseReason: null, + companyId: "company-1", + name: "Budget Agent", + }], + [{ + status: "paused", + pauseReason: "budget", + name: "Paperclip", + }], + ]); + + const service = budgetService(dbStub.db as any); + const block = await service.getInvocationBlock("company-1", "agent-1"); + + expect(block).toEqual({ + scopeType: "company", + scopeId: "company-1", + scopeName: "Paperclip", + reason: "Company is paused because its budget hard-stop was reached.", + }); + }); + + it("uses live observed spend when raising a budget incident", async () => { + const dbStub = createDbStub([ + [{ + id: "incident-1", + companyId: "company-1", + policyId: "policy-1", + amountObserved: 120, + approvalId: "approval-1", + }], + [{ + id: "policy-1", + companyId: "company-1", + scopeType: "company", + scopeId: "company-1", + metric: "billed_cents", + windowKind: "calendar_month_utc", + }], + [{ total: 150 }], + ]); + + const service = budgetService(dbStub.db as any); + + await expect( + service.resolveIncident( + "company-1", + "incident-1", + { action: "raise_budget_and_resume", amount: 140 }, + "board-user", + ), + ).rejects.toThrow("New budget must exceed current observed spend"); + }); + + it("syncs company monthly budget when raising and resuming a company incident", async () => { + const now = new Date(); + const dbStub = createDbStub([ + [{ + id: "incident-1", + companyId: "company-1", + policyId: "policy-1", + scopeType: "company", + scopeId: "company-1", + metric: "billed_cents", + windowKind: "calendar_month_utc", + windowStart: now, + windowEnd: now, + thresholdType: "hard", + amountLimit: 100, + amountObserved: 120, + status: "open", + approvalId: "approval-1", + resolvedAt: null, + createdAt: now, + updatedAt: now, + }], + [{ + id: "policy-1", + companyId: "company-1", + scopeType: "company", + scopeId: "company-1", + metric: "billed_cents", + windowKind: "calendar_month_utc", + amount: 100, + }], + [{ total: 120 }], + [{ id: "approval-1", status: "approved" }], + [{ + companyId: "company-1", + name: "Paperclip", + status: "paused", + pauseReason: "budget", + pausedAt: now, + }], + ]); + + const service = budgetService(dbStub.db as any); + await service.resolveIncident( + "company-1", + "incident-1", + { action: "raise_budget_and_resume", amount: 175 }, + "board-user", + ); + + expect(dbStub.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ + budgetMonthlyCents: 175, + updatedAt: expect.any(Date), + }), + ); + }); +}); 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 new file mode 100644 index 00000000..517ada52 --- /dev/null +++ b/server/src/__tests__/costs-service.test.ts @@ -0,0 +1,226 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { costRoutes } from "../routes/costs.js"; +import { errorHandler } from "../middleware/index.js"; + +function makeDb(overrides: Record = {}) { + const selectChain = { + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + leftJoin: vi.fn().mockReturnThis(), + innerJoin: vi.fn().mockReturnThis(), + groupBy: vi.fn().mockReturnThis(), + orderBy: vi.fn().mockReturnThis(), + limit: vi.fn().mockReturnThis(), + then: vi.fn().mockResolvedValue([]), + }; + + const thenableChain = Object.assign(Promise.resolve([]), selectChain); + + return { + select: vi.fn().mockReturnValue(thenableChain), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([]) }), + }), + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }), + }), + ...overrides, + }; +} + +const mockCompanyService = vi.hoisted(() => ({ + getById: vi.fn(), + update: vi.fn(), +})); +const mockAgentService = vi.hoisted(() => ({ + getById: vi.fn(), + update: vi.fn(), +})); +const mockHeartbeatService = vi.hoisted(() => ({ + cancelBudgetScopeWork: vi.fn().mockResolvedValue(undefined), +})); +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", () => ({ + budgetService: () => mockBudgetService, + costService: () => mockCostService, + financeService: () => mockFinanceService, + companyService: () => mockCompanyService, + agentService: () => mockAgentService, + heartbeatService: () => mockHeartbeatService, + logActivity: mockLogActivity, +})); + +vi.mock("../services/quota-windows.js", () => ({ + fetchAllQuotaWindows: mockFetchAllQuotaWindows, +})); + +function createApp() { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + req.actor = { type: "board", userId: "board-user", source: "local_implicit" }; + next(); + }); + app.use("/api", costRoutes(makeDb() as any)); + app.use(errorHandler); + return app; +} + +function createAppWithActor(actor: any) { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + req.actor = actor; + next(); + }); + app.use("/api", costRoutes(makeDb() as any)); + app.use(errorHandler); + return app; +} + +beforeEach(() => { + vi.clearAllMocks(); + mockCompanyService.update.mockResolvedValue({ + id: "company-1", + name: "Paperclip", + budgetMonthlyCents: 100, + spentMonthlyCents: 0, + }); + mockAgentService.update.mockResolvedValue({ + id: "agent-1", + companyId: "company-1", + name: "Budget Agent", + budgetMonthlyCents: 100, + spentMonthlyCents: 0, + }); + mockBudgetService.upsertPolicy.mockResolvedValue(undefined); +}); + +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") + .query({ from: "2026-01-01T00:00:00.000Z", to: "2026-01-31T23:59:59.999Z" }); + expect(res.status).toBe(200); + }); + + it("returns 400 for an invalid 'from' date string", async () => { + const app = createApp(); + const res = await request(app) + .get("/api/companies/company-1/costs/summary") + .query({ from: "not-a-date" }); + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/invalid 'from' date/i); + }); + + it("returns 400 for an invalid 'to' date string", async () => { + const app = createApp(); + const res = await request(app) + .get("/api/companies/company-1/costs/summary") + .query({ to: "banana" }); + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/invalid 'to' date/i); + }); + + it("returns finance summary rows for valid requests", async () => { + const app = createApp(); + 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); + 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); + }); + + it("rejects company budget updates for board users outside the company", async () => { + const app = createAppWithActor({ + type: "board", + userId: "board-user", + source: "session", + isInstanceAdmin: false, + companyIds: ["company-2"], + }); + + const res = await request(app) + .patch("/api/companies/company-1/budgets") + .send({ budgetMonthlyCents: 2500 }); + + expect(res.status).toBe(403); + expect(mockCompanyService.update).not.toHaveBeenCalled(); + }); + + it("rejects agent budget updates for board users outside the agent company", async () => { + mockAgentService.getById.mockResolvedValue({ + id: "agent-1", + companyId: "company-1", + name: "Budget Agent", + budgetMonthlyCents: 100, + spentMonthlyCents: 0, + }); + const app = createAppWithActor({ + type: "board", + userId: "board-user", + source: "session", + isInstanceAdmin: false, + companyIds: ["company-2"], + }); + + const res = await request(app) + .patch("/api/agents/agent-1/budgets") + .send({ budgetMonthlyCents: 2500 }); + + expect(res.status).toBe(403); + expect(mockAgentService.update).not.toHaveBeenCalled(); + }); +}); diff --git a/server/src/__tests__/monthly-spend-service.test.ts b/server/src/__tests__/monthly-spend-service.test.ts new file mode 100644 index 00000000..97b213af --- /dev/null +++ b/server/src/__tests__/monthly-spend-service.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { companyService } from "../services/companies.ts"; +import { agentService } from "../services/agents.ts"; + +function createSelectSequenceDb(results: unknown[]) { + const pending = [...results]; + const chain = { + from: vi.fn(() => chain), + where: vi.fn(() => chain), + leftJoin: vi.fn(() => chain), + groupBy: vi.fn(() => chain), + then: vi.fn((resolve: (value: unknown[]) => unknown) => Promise.resolve(resolve(pending.shift() ?? []))), + }; + + return { + db: { + select: vi.fn(() => chain), + }, + }; +} + +describe("monthly spend hydration", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("recomputes company spentMonthlyCents from the current utc month instead of returning stale stored values", async () => { + const dbStub = createSelectSequenceDb([ + [{ + id: "company-1", + name: "Paperclip", + description: null, + status: "active", + issuePrefix: "PAP", + issueCounter: 1, + budgetMonthlyCents: 5000, + spentMonthlyCents: 999999, + requireBoardApprovalForNewAgents: false, + brandColor: null, + logoAssetId: null, + createdAt: new Date(), + updatedAt: new Date(), + }], + [{ + companyId: "company-1", + spentMonthlyCents: 420, + }], + ]); + + const companies = companyService(dbStub.db as any); + const [company] = await companies.list(); + + expect(company.spentMonthlyCents).toBe(420); + }); + + it("recomputes agent spentMonthlyCents from the current utc month instead of returning stale stored values", async () => { + const dbStub = createSelectSequenceDb([ + [{ + id: "agent-1", + companyId: "company-1", + name: "Budget Agent", + role: "general", + title: null, + reportsTo: null, + capabilities: null, + adapterType: "claude-local", + adapterConfig: {}, + runtimeConfig: {}, + budgetMonthlyCents: 5000, + spentMonthlyCents: 999999, + metadata: null, + permissions: null, + status: "idle", + pauseReason: null, + pausedAt: null, + createdAt: new Date(), + updatedAt: new Date(), + }], + [{ + agentId: "agent-1", + spentMonthlyCents: 175, + }], + ]); + + const agents = agentService(dbStub.db as any); + const agent = await agents.getById("agent-1"); + + expect(agent?.spentMonthlyCents).toBe(175); + }); +}); 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 new file mode 100644 index 00000000..c9d0fd92 --- /dev/null +++ b/server/src/__tests__/quota-windows.test.ts @@ -0,0 +1,812 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import os from "node:os"; +import path from "node:path"; +import type { QuotaWindow } from "@paperclipai/adapter-utils"; + +// Pure utility functions — import directly from adapter source +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"; + +// --------------------------------------------------------------------------- +// toPercent +// --------------------------------------------------------------------------- + +describe("toPercent", () => { + it("returns null for null input", () => { + expect(toPercent(null)).toBe(null); + }); + + it("returns null for undefined input", () => { + expect(toPercent(undefined)).toBe(null); + }); + + it("converts 0 to 0", () => { + expect(toPercent(0)).toBe(0); + }); + + it("converts 0.5 to 50", () => { + expect(toPercent(0.5)).toBe(50); + }); + + it("converts 1.0 to 100", () => { + expect(toPercent(1.0)).toBe(100); + }); + + it("clamps overshoot to 100", () => { + // floating-point utilization can slightly exceed 1.0 + expect(toPercent(1.001)).toBe(100); + expect(toPercent(1.01)).toBe(100); + }); + + it("rounds to nearest integer", () => { + expect(toPercent(0.333)).toBe(33); + expect(toPercent(0.666)).toBe(67); + }); +}); + +// --------------------------------------------------------------------------- +// secondsToWindowLabel +// --------------------------------------------------------------------------- + +describe("secondsToWindowLabel", () => { + it("returns fallback for null seconds", () => { + expect(secondsToWindowLabel(null, "Primary")).toBe("Primary"); + }); + + it("returns fallback for undefined seconds", () => { + expect(secondsToWindowLabel(undefined, "Secondary")).toBe("Secondary"); + }); + + it("labels windows under 6 hours as '5h'", () => { + expect(secondsToWindowLabel(3600, "fallback")).toBe("5h"); // 1h + expect(secondsToWindowLabel(18000, "fallback")).toBe("5h"); // 5h exactly + }); + + it("labels windows up to 24 hours as '24h'", () => { + expect(secondsToWindowLabel(21600, "fallback")).toBe("24h"); // 6h (≥6h boundary) + expect(secondsToWindowLabel(86400, "fallback")).toBe("24h"); // 24h exactly + }); + + it("labels windows up to 7 days as '7d'", () => { + expect(secondsToWindowLabel(86401, "fallback")).toBe("7d"); // just over 24h + expect(secondsToWindowLabel(604800, "fallback")).toBe("7d"); // 7d exactly + }); + + it("labels windows beyond 7 days with actual day count", () => { + expect(secondsToWindowLabel(1209600, "fallback")).toBe("14d"); // 14d + expect(secondsToWindowLabel(2592000, "fallback")).toBe("30d"); // 30d + }); +}); + +// --------------------------------------------------------------------------- +// WHAM used_percent normalization (codex / openai) +// --------------------------------------------------------------------------- + +describe("WHAM used_percent normalization via fetchCodexQuota", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + function mockFetch(body: unknown) { + (fetch as ReturnType).mockResolvedValue({ + ok: true, + json: async () => body, + } as Response); + } + + it("treats values >= 1 as already-percentage (50 → 50%)", async () => { + mockFetch({ + rate_limit: { + primary_window: { + used_percent: 50, + limit_window_seconds: 18000, + reset_at: null, + }, + }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows[0]!.usedPercent).toBe(50); + }); + + it("treats values < 1 as fraction and multiplies by 100 (0.5 → 50%)", async () => { + mockFetch({ + rate_limit: { + primary_window: { + used_percent: 0.5, + limit_window_seconds: 18000, + reset_at: null, + }, + }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows[0]!.usedPercent).toBe(50); + }); + + it("treats value exactly 1.0 as 1% (not 100%) — the < 1 heuristic boundary", async () => { + // 1.0 is NOT < 1, so it is treated as already-percentage → 1% + mockFetch({ + rate_limit: { + primary_window: { + used_percent: 1.0, + limit_window_seconds: 18000, + reset_at: null, + }, + }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows[0]!.usedPercent).toBe(1); + }); + + it("treats value 0 as 0%", async () => { + mockFetch({ + rate_limit: { + primary_window: { + used_percent: 0, + limit_window_seconds: 18000, + reset_at: null, + }, + }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows[0]!.usedPercent).toBe(0); + }); + + it("clamps 100% to 100 (no overshoot)", async () => { + mockFetch({ + rate_limit: { + primary_window: { + used_percent: 105, + limit_window_seconds: 18000, + reset_at: null, + }, + }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows[0]!.usedPercent).toBe(100); + }); + + it("sets usedPercent to null when used_percent is absent", async () => { + mockFetch({ + rate_limit: { + primary_window: { + limit_window_seconds: 18000, + reset_at: null, + }, + }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows[0]!.usedPercent).toBe(null); + }); +}); + +// --------------------------------------------------------------------------- +// readClaudeToken — filesystem paths +// --------------------------------------------------------------------------- + +describe("readClaudeToken", () => { + const savedEnv = process.env.CLAUDE_CONFIG_DIR; + + afterEach(() => { + if (savedEnv === undefined) { + delete process.env.CLAUDE_CONFIG_DIR; + } else { + process.env.CLAUDE_CONFIG_DIR = savedEnv; + } + vi.restoreAllMocks(); + }); + + it("returns null when credentials.json does not exist", async () => { + // Point to a directory that does not have credentials.json + process.env.CLAUDE_CONFIG_DIR = "/tmp/__no_such_paperclip_dir__"; + const token = await readClaudeToken(); + expect(token).toBe(null); + }); + + it("returns null for malformed JSON", async () => { + const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); + await import("node:fs/promises").then((fs) => + fs.mkdir(tmpDir, { recursive: true }).then(() => + fs.writeFile(path.join(tmpDir, "credentials.json"), "not-json"), + ), + ); + process.env.CLAUDE_CONFIG_DIR = tmpDir; + const token = await readClaudeToken(); + expect(token).toBe(null); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); + + it("returns null when claudeAiOauth key is missing", async () => { + const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); + await import("node:fs/promises").then((fs) => + fs.mkdir(tmpDir, { recursive: true }).then(() => + fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify({ other: "data" })), + ), + ); + process.env.CLAUDE_CONFIG_DIR = tmpDir; + const token = await readClaudeToken(); + expect(token).toBe(null); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); + + it("returns null when accessToken is an empty string", async () => { + const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); + const creds = { claudeAiOauth: { accessToken: "" } }; + 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(null); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); + + it("returns the token when credentials file is well-formed", async () => { + const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); + const creds = { claudeAiOauth: { accessToken: "my-test-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("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`.", + ); + }); +}); + +// --------------------------------------------------------------------------- +// readCodexAuthInfo / readCodexToken — filesystem paths +// --------------------------------------------------------------------------- + +describe("readCodexAuthInfo", () => { + const savedEnv = process.env.CODEX_HOME; + + afterEach(() => { + if (savedEnv === undefined) { + delete process.env.CODEX_HOME; + } else { + process.env.CODEX_HOME = savedEnv; + } + }); + + it("returns null when auth.json does not exist", async () => { + process.env.CODEX_HOME = "/tmp/__no_such_paperclip_codex_dir__"; + const result = await readCodexAuthInfo(); + expect(result).toBe(null); + }); + + it("returns null for malformed JSON", 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"), "{bad json"), + ), + ); + process.env.CODEX_HOME = tmpDir; + const result = await readCodexAuthInfo(); + expect(result).toBe(null); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); + + it("returns null when accessToken is absent", 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({ accountId: "acc-1" })), + ), + ); + process.env.CODEX_HOME = tmpDir; + const result = await readCodexAuthInfo(); + expect(result).toBe(null); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); + + 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) => + 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: "codex-token", + accountId: "acc-123", + email: null, + planType: null, + }); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); + + 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({ + tokens: { + access_token: "nested-token", + account_id: "acc-nested", + }, + })), + ), + ); + process.env.CODEX_HOME = tmpDir; + const result = await readCodexToken(); + expect(result).toEqual({ token: "nested-token", accountId: "acc-nested" }); + await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); + }); +}); + +// --------------------------------------------------------------------------- +// fetchClaudeQuota — response parsing +// --------------------------------------------------------------------------- + +describe("fetchClaudeQuota", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + function mockFetch(body: unknown, ok = true, status = 200) { + (fetch as ReturnType).mockResolvedValue({ + ok, + status, + json: async () => body, + } as Response); + } + + it("throws when the API returns a non-200 status", async () => { + mockFetch({}, false, 401); + await expect(fetchClaudeQuota("token")).rejects.toThrow("anthropic usage api returned 401"); + }); + + it("returns an empty array when all window fields are absent", async () => { + mockFetch({}); + const windows = await fetchClaudeQuota("token"); + expect(windows).toEqual([]); + }); + + it("parses five_hour window", async () => { + 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: "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: "Current week (all models)", + usedPercent: 75, + resetsAt: null, + }); + }); + + it("parses seven_day_sonnet and seven_day_opus windows", async () => { + mockFetch({ + seven_day_sonnet: { utilization: 0.2, resets_at: null }, + seven_day_opus: { utilization: 0.9, resets_at: null }, + }); + const windows = await fetchClaudeQuota("token"); + expect(windows).toHaveLength(2); + 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 () => { + mockFetch({ five_hour: { resets_at: null } }); + const windows = await fetchClaudeQuota("token"); + expect(windows[0]!.usedPercent).toBe(null); + }); + + it("includes all four windows when all are present", async () => { + mockFetch({ + five_hour: { utilization: 0.1, resets_at: null }, + seven_day: { utilization: 0.2, resets_at: null }, + seven_day_sonnet: { utilization: 0.3, resets_at: null }, + seven_day_opus: { utilization: 0.4, resets_at: null }, + }); + const windows = await fetchClaudeQuota("token"); + expect(windows).toHaveLength(4); + const labels = windows.map((w: QuotaWindow) => w.label); + 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", + }, + ]); + }); +}); + +// --------------------------------------------------------------------------- +// fetchCodexQuota — response parsing (credits, windows) +// --------------------------------------------------------------------------- + +describe("fetchCodexQuota", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + function mockFetch(body: unknown, ok = true, status = 200) { + (fetch as ReturnType).mockResolvedValue({ + ok, + status, + json: async () => body, + } as Response); + } + + it("throws when the WHAM API returns a non-200 status", async () => { + mockFetch({}, false, 403); + await expect(fetchCodexQuota("token", null)).rejects.toThrow("chatgpt wham api returned 403"); + }); + + it("passes ChatGPT-Account-Id header when accountId is provided", async () => { + mockFetch({}); + await fetchCodexQuota("token", "acc-xyz"); + const callInit = (fetch as ReturnType).mock.calls[0][1] as RequestInit; + expect((callInit.headers as Record)["ChatGPT-Account-Id"]).toBe("acc-xyz"); + }); + + it("omits ChatGPT-Account-Id header when accountId is null", async () => { + mockFetch({}); + await fetchCodexQuota("token", null); + const callInit = (fetch as ReturnType).mock.calls[0][1] as RequestInit; + expect((callInit.headers as Record)["ChatGPT-Account-Id"]).toBeUndefined(); + }); + + it("returns empty array when response body is empty", async () => { + mockFetch({}); + const windows = await fetchCodexQuota("token", null); + expect(windows).toEqual([]); + }); + + it("normalizes numeric reset timestamps from WHAM", async () => { + mockFetch({ + rate_limit: { + 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: "5h limit", usedPercent: 30, resetsAt: "2026-01-02T00:00:00.000Z" }); + }); + + it("parses secondary_window alongside primary_window", async () => { + mockFetch({ + rate_limit: { + primary_window: { used_percent: 10, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 60, limit_window_seconds: 604800 }, + }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows).toHaveLength(2); + 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 () => { + mockFetch({ + credits: { balance: 420, unlimited: false }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows).toHaveLength(1); + expect(windows[0]).toMatchObject({ label: "Credits", valueLabel: "$4.20 remaining", usedPercent: null }); + }); + + it("omits Credits window when unlimited is true", async () => { + mockFetch({ + credits: { balance: 9999, unlimited: true }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows).toEqual([]); + }); + + it("shows 'N/A' valueLabel when credits balance is null", async () => { + mockFetch({ + credits: { balance: null, unlimited: false }, + }); + const windows = await fetchCodexQuota("token", null); + expect(windows[0]!.valueLabel).toBe("N/A"); + }); +}); + +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 +// --------------------------------------------------------------------------- + +describe("fetchWithTimeout", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.useRealTimers(); + }); + + it("resolves normally when fetch completes before timeout", async () => { + const mockResponse = { ok: true, status: 200, json: async () => ({}) } as Response; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse)); + + const result = await fetchWithTimeout("https://example.com", {}, 5000); + expect(result.ok).toBe(true); + }); + + it("rejects with abort error when fetch takes too long", async () => { + vi.useFakeTimers(); + vi.stubGlobal( + "fetch", + vi.fn().mockImplementation( + (_url: string, init: RequestInit) => + new Promise((_resolve, reject) => { + init.signal?.addEventListener("abort", () => { + reject(new DOMException("The operation was aborted.", "AbortError")); + }); + }), + ), + ); + + const promise = fetchWithTimeout("https://example.com", {}, 1000); + vi.advanceTimersByTime(1001); + await expect(promise).rejects.toThrow("aborted"); + }); +}); diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 3e49e450..a25ebf7e 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -5,6 +5,7 @@ import { syncClaudeSkills, testEnvironment as claudeTestEnvironment, sessionCodec as claudeSessionCodec, + getQuotaWindows as claudeGetQuotaWindows, } from "@paperclipai/adapter-claude-local/server"; import { agentConfigurationDoc as claudeAgentConfigurationDoc, models as claudeModels } from "@paperclipai/adapter-claude-local"; import { @@ -13,6 +14,7 @@ import { syncCodexSkills, testEnvironment as codexTestEnvironment, sessionCodec as codexSessionCodec, + getQuotaWindows as codexGetQuotaWindows, } from "@paperclipai/adapter-codex-local/server"; import { agentConfigurationDoc as codexAgentConfigurationDoc, models as codexModels } from "@paperclipai/adapter-codex-local"; import { @@ -85,6 +87,7 @@ const claudeLocalAdapter: ServerAdapterModule = { models: claudeModels, supportsLocalAgentJwt: true, agentConfigurationDoc: claudeAgentConfigurationDoc, + getQuotaWindows: claudeGetQuotaWindows, }; const codexLocalAdapter: ServerAdapterModule = { @@ -98,6 +101,7 @@ const codexLocalAdapter: ServerAdapterModule = { listModels: listCodexModels, supportsLocalAgentJwt: true, agentConfigurationDoc: codexAgentConfigurationDoc, + getQuotaWindows: codexGetQuotaWindows, }; const cursorLocalAdapter: ServerAdapterModule = { diff --git a/server/src/index.ts b/server/src/index.ts index 27b559eb..f75a0e6f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -83,8 +83,7 @@ export async function startServer(): Promise { | "skipped" | "already applied" | "applied (empty database)" - | "applied (pending migrations)" - | "pending migrations skipped"; + | "applied (pending migrations)"; function formatPendingMigrationSummary(migrations: string[]): string { if (migrations.length === 0) return "none"; @@ -139,11 +138,10 @@ export async function startServer(): Promise { ); const apply = autoApply ? true : await promptApplyMigrations(state.pendingMigrations); if (!apply) { - logger.warn( - { pendingMigrations: state.pendingMigrations }, - `${label} has pending migrations; continuing without applying. Run pnpm db:migrate to apply before startup.`, + throw new Error( + `${label} has pending migrations (${formatPendingMigrationSummary(state.pendingMigrations)}). ` + + "Refusing to start against a stale schema. Run pnpm db:migrate or set PAPERCLIP_MIGRATION_AUTO_APPLY=true.", ); - return "pending migrations skipped"; } logger.info({ pendingMigrations: state.pendingMigrations }, `Applying ${state.pendingMigrations.length} pending migrations for ${label}`); @@ -153,11 +151,10 @@ export async function startServer(): Promise { const apply = autoApply ? true : await promptApplyMigrations(state.pendingMigrations); if (!apply) { - logger.warn( - { pendingMigrations: state.pendingMigrations }, - `${label} has pending migrations; continuing without applying. Run pnpm db:migrate to apply before startup.`, + throw new Error( + `${label} has pending migrations (${formatPendingMigrationSummary(state.pendingMigrations)}). ` + + "Refusing to start against a stale schema. Run pnpm db:migrate or set PAPERCLIP_MIGRATION_AUTO_APPLY=true.", ); - return "pending migrations skipped"; } logger.info({ pendingMigrations: state.pendingMigrations }, `Applying ${state.pendingMigrations.length} pending migrations for ${label}`); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index a64ca042..549cef60 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -30,6 +30,7 @@ import { accessService, approvalService, companySkillService, + budgetService, heartbeatService, issueApprovalService, issueService, @@ -64,6 +65,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); @@ -1094,6 +1096,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/assets.ts b/server/src/routes/assets.ts index bd2f154d..0a6f857a 100644 --- a/server/src/routes/assets.ts +++ b/server/src/routes/assets.ts @@ -1,21 +1,104 @@ import { Router, type Request, type Response } from "express"; import multer from "multer"; +import createDOMPurify from "dompurify"; +import { JSDOM } from "jsdom"; import type { Db } from "@paperclipai/db"; import { createAssetImageMetadataSchema } from "@paperclipai/shared"; import type { StorageService } from "../storage/types.js"; import { assetService, logActivity } from "../services/index.js"; -import { assertCompanyAccess, getActorInfo } from "./authz.js"; import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js"; +import { assertCompanyAccess, getActorInfo } from "./authz.js"; +const SVG_CONTENT_TYPE = "image/svg+xml"; +const ALLOWED_COMPANY_LOGO_CONTENT_TYPES = new Set([ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/gif", + SVG_CONTENT_TYPE, +]); + +function sanitizeSvgBuffer(input: Buffer): Buffer | null { + const raw = input.toString("utf8").trim(); + if (!raw) return null; + + const baseDom = new JSDOM(""); + const domPurify = createDOMPurify( + baseDom.window as unknown as Parameters[0], + ); + domPurify.addHook("uponSanitizeAttribute", (_node, data) => { + const attrName = data.attrName.toLowerCase(); + const attrValue = (data.attrValue ?? "").trim(); + + if (attrName.startsWith("on")) { + data.keepAttr = false; + return; + } + + if ((attrName === "href" || attrName === "xlink:href") && attrValue && !attrValue.startsWith("#")) { + data.keepAttr = false; + } + }); + + let parsedDom: JSDOM | null = null; + try { + const sanitized = domPurify.sanitize(raw, { + USE_PROFILES: { svg: true, svgFilters: true, html: false }, + FORBID_TAGS: ["script", "foreignObject"], + FORBID_CONTENTS: ["script", "foreignObject"], + RETURN_TRUSTED_TYPE: false, + }); + + parsedDom = new JSDOM(sanitized, { contentType: SVG_CONTENT_TYPE }); + const document = parsedDom.window.document; + const root = document.documentElement; + if (!root || root.tagName.toLowerCase() !== "svg") return null; + + for (const el of Array.from(root.querySelectorAll("script, foreignObject"))) { + el.remove(); + } + for (const el of Array.from(root.querySelectorAll("*"))) { + for (const attr of Array.from(el.attributes)) { + const attrName = attr.name.toLowerCase(); + const attrValue = attr.value.trim(); + if (attrName.startsWith("on")) { + el.removeAttribute(attr.name); + continue; + } + if ((attrName === "href" || attrName === "xlink:href") && attrValue && !attrValue.startsWith("#")) { + el.removeAttribute(attr.name); + } + } + } + + const output = root.outerHTML.trim(); + if (!output || !/^]/i.test(output)) return null; + return Buffer.from(output, "utf8"); + } catch { + return null; + } finally { + parsedDom?.window.close(); + baseDom.window.close(); + } +} export function assetRoutes(db: Db, storage: StorageService) { const router = Router(); const svc = assetService(db); - const upload = multer({ + const assetUpload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 }, + }); + const companyLogoUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 }, }); - async function runSingleFileUpload(req: Request, res: Response) { + async function runSingleFileUpload( + upload: ReturnType, + req: Request, + res: Response, + ) { await new Promise((resolve, reject) => { upload.single("file")(req, res, (err: unknown) => { if (err) reject(err); @@ -29,7 +112,7 @@ export function assetRoutes(db: Db, storage: StorageService) { assertCompanyAccess(req, companyId); try { - await runSingleFileUpload(req, res); + await runSingleFileUpload(assetUpload, req, res); } catch (err) { if (err instanceof multer.MulterError) { if (err.code === "LIMIT_FILE_SIZE") { @@ -48,16 +131,6 @@ export function assetRoutes(db: Db, storage: StorageService) { return; } - const contentType = (file.mimetype || "").toLowerCase(); - if (!isAllowedContentType(contentType)) { - res.status(422).json({ error: `Unsupported file type: ${contentType || "unknown"}` }); - return; - } - if (file.buffer.length <= 0) { - res.status(422).json({ error: "Image is empty" }); - return; - } - const parsedMeta = createAssetImageMetadataSchema.safeParse(req.body ?? {}); if (!parsedMeta.success) { res.status(400).json({ error: "Invalid image metadata", details: parsedMeta.error.issues }); @@ -65,13 +138,32 @@ export function assetRoutes(db: Db, storage: StorageService) { } const namespaceSuffix = parsedMeta.data.namespace ?? "general"; + const contentType = (file.mimetype || "").toLowerCase(); + if (contentType !== SVG_CONTENT_TYPE && !isAllowedContentType(contentType)) { + res.status(422).json({ error: `Unsupported file type: ${contentType || "unknown"}` }); + return; + } + let fileBody = file.buffer; + if (contentType === SVG_CONTENT_TYPE) { + const sanitized = sanitizeSvgBuffer(file.buffer); + if (!sanitized || sanitized.length <= 0) { + res.status(422).json({ error: "SVG could not be sanitized" }); + return; + } + fileBody = sanitized; + } + if (fileBody.length <= 0) { + res.status(422).json({ error: "Image is empty" }); + return; + } + const actor = getActorInfo(req); const stored = await storage.putFile({ companyId, namespace: `assets/${namespaceSuffix}`, originalFilename: file.originalname || null, contentType, - body: file.buffer, + body: fileBody, }); const asset = await svc.create(companyId, { @@ -118,6 +210,105 @@ export function assetRoutes(db: Db, storage: StorageService) { }); }); + router.post("/companies/:companyId/logo", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + + try { + await runSingleFileUpload(companyLogoUpload, req, res); + } catch (err) { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + res.status(422).json({ error: `Image exceeds ${MAX_ATTACHMENT_BYTES} bytes` }); + return; + } + res.status(400).json({ error: err.message }); + return; + } + throw err; + } + + const file = (req as Request & { file?: { mimetype: string; buffer: Buffer; originalname: string } }).file; + if (!file) { + res.status(400).json({ error: "Missing file field 'file'" }); + return; + } + + const contentType = (file.mimetype || "").toLowerCase(); + if (!ALLOWED_COMPANY_LOGO_CONTENT_TYPES.has(contentType)) { + res.status(422).json({ error: `Unsupported image type: ${contentType || "unknown"}` }); + return; + } + + let fileBody = file.buffer; + if (contentType === SVG_CONTENT_TYPE) { + const sanitized = sanitizeSvgBuffer(file.buffer); + if (!sanitized || sanitized.length <= 0) { + res.status(422).json({ error: "SVG could not be sanitized" }); + return; + } + fileBody = sanitized; + } + + if (fileBody.length <= 0) { + res.status(422).json({ error: "Image is empty" }); + return; + } + + const actor = getActorInfo(req); + const stored = await storage.putFile({ + companyId, + namespace: "assets/companies", + originalFilename: file.originalname || null, + contentType, + body: fileBody, + }); + + const asset = await svc.create(companyId, { + provider: stored.provider, + objectKey: stored.objectKey, + contentType: stored.contentType, + byteSize: stored.byteSize, + sha256: stored.sha256, + originalFilename: stored.originalFilename, + createdByAgentId: actor.agentId, + createdByUserId: actor.actorType === "user" ? actor.actorId : null, + }); + + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "asset.created", + entityType: "asset", + entityId: asset.id, + details: { + originalFilename: asset.originalFilename, + contentType: asset.contentType, + byteSize: asset.byteSize, + namespace: "assets/companies", + }, + }); + + res.status(201).json({ + assetId: asset.id, + companyId: asset.companyId, + provider: asset.provider, + objectKey: asset.objectKey, + contentType: asset.contentType, + byteSize: asset.byteSize, + sha256: asset.sha256, + originalFilename: asset.originalFilename, + createdByAgentId: asset.createdByAgentId, + createdByUserId: asset.createdByUserId, + createdAt: asset.createdAt, + updatedAt: asset.updatedAt, + contentPath: `/api/assets/${asset.id}/content`, + }); + }); + router.get("/assets/:assetId/content", async (req, res, next) => { const assetId = req.params.assetId as string; const asset = await svc.getById(assetId); @@ -128,9 +319,14 @@ export function assetRoutes(db: Db, storage: StorageService) { assertCompanyAccess(req, asset.companyId); const object = await storage.getObject(asset.companyId, asset.objectKey); - res.setHeader("Content-Type", asset.contentType || object.contentType || "application/octet-stream"); + const responseContentType = asset.contentType || object.contentType || "application/octet-stream"; + res.setHeader("Content-Type", responseContentType); res.setHeader("Content-Length", String(asset.byteSize || object.contentLength || 0)); res.setHeader("Cache-Control", "private, max-age=60"); + res.setHeader("X-Content-Type-Options", "nosniff"); + if (responseContentType === SVG_CONTENT_TYPE) { + res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'"); + } const filename = asset.originalFilename ?? "asset"; res.setHeader("Content-Disposition", `inline; filename=\"${filename.replaceAll("\"", "")}\"`); @@ -142,4 +338,3 @@ export function assetRoutes(db: Db, storage: StorageService) { return router; } - 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 e4527bff..82925bd7 100644 --- a/server/src/routes/costs.ts +++ b/server/src/routes/costs.ts @@ -1,13 +1,35 @@ 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, + heartbeatService, + logActivity, +} from "../services/index.js"; import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js"; +import { fetchAllQuotaWindows } from "../services/quota-windows.js"; +import { badRequest } from "../errors.js"; export function costRoutes(db: Db) { const router = Router(); - const costs = costService(db); + const heartbeat = heartbeatService(db); + const budgetHooks = { + cancelWorkForScope: heartbeat.cancelBudgetScopeWork, + }; + const costs = costService(db, budgetHooks); + const finance = financeService(db); + const budgets = budgetService(db, budgetHooks); const companies = companyService(db); const agents = agentService(db); @@ -40,12 +62,56 @@ 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 from = query.from ? new Date(query.from as string) : undefined; - const to = query.to ? new Date(query.to as string) : undefined; + const fromRaw = query.from as string | undefined; + const toRaw = query.to as string | undefined; + const from = fromRaw ? new Date(fromRaw) : undefined; + const to = toRaw ? new Date(toRaw) : undefined; + if (from && isNaN(from.getTime())) throw badRequest("invalid 'from' date"); + if (to && isNaN(to.getTime())) throw badRequest("invalid 'to' date"); 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); @@ -62,6 +128,117 @@ export function costRoutes(db: Db) { res.json(rows); }); + router.get("/companies/:companyId/costs/by-agent-model", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const range = parseDateRange(req.query); + const rows = await costs.byAgentModel(companyId, range); + res.json(rows); + }); + + router.get("/companies/:companyId/costs/by-provider", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const range = parseDateRange(req.query); + const rows = await costs.byProvider(companyId, range); + 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); + const rows = await costs.windowSpend(companyId); + res.json(rows); + }); + + router.get("/companies/:companyId/costs/quota-windows", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + assertBoard(req); + // validate companyId resolves to a real company so the "__none__" sentinel + // and any forged ids are rejected before we touch provider credentials + const company = await companies.getById(companyId); + if (!company) { + res.status(404).json({ error: "Company not found" }); + return; + } + const results = await fetchAllQuotaWindows(); + 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); @@ -73,6 +250,7 @@ export function costRoutes(db: Db) { router.patch("/companies/:companyId/budgets", validate(updateBudgetSchema), async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); const company = await companies.update(companyId, { budgetMonthlyCents: req.body.budgetMonthlyCents }); if (!company) { res.status(404).json({ error: "Company not found" }); @@ -89,6 +267,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); }); @@ -100,6 +289,8 @@ export function costRoutes(db: Db) { return; } + assertCompanyAccess(req, agent.companyId); + if (req.actor.type === "agent") { if (req.actor.agentId !== agentId) { res.status(403).json({ error: "Agent can only change its own budget" }); @@ -125,6 +316,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..17d2e46d 100644 --- a/server/src/services/agents.ts +++ b/server/src/services/agents.ts @@ -1,5 +1,5 @@ import { createHash, randomBytes } from "node:crypto"; -import { and, desc, eq, inArray, ne } from "drizzle-orm"; +import { and, desc, eq, gte, inArray, lt, ne, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents, @@ -8,6 +8,7 @@ import { agentRuntimeState, agentTaskSessions, agentWakeupRequests, + costEvents, heartbeatRunEvents, heartbeatRuns, } from "@paperclipai/db"; @@ -182,6 +183,15 @@ export function deduplicateAgentName( } export function agentService(db: Db) { + function currentUtcMonthWindow(now = new Date()) { + const year = now.getUTCFullYear(); + const month = now.getUTCMonth(); + return { + start: new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)), + end: new Date(Date.UTC(year, month + 1, 1, 0, 0, 0, 0)), + }; + } + function withUrlKey(row: T) { return { ...row, @@ -196,13 +206,47 @@ export function agentService(db: Db) { }); } + async function getMonthlySpendByAgentIds(companyId: string, agentIds: string[]) { + if (agentIds.length === 0) return new Map(); + const { start, end } = currentUtcMonthWindow(); + const rows = await db + .select({ + agentId: costEvents.agentId, + spentMonthlyCents: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, + }) + .from(costEvents) + .where( + and( + eq(costEvents.companyId, companyId), + inArray(costEvents.agentId, agentIds), + gte(costEvents.occurredAt, start), + lt(costEvents.occurredAt, end), + ), + ) + .groupBy(costEvents.agentId); + return new Map(rows.map((row) => [row.agentId, Number(row.spentMonthlyCents ?? 0)])); + } + + async function hydrateAgentSpend(rows: T[]) { + const agentIds = rows.map((row) => row.id); + const companyId = rows[0]?.companyId; + if (!companyId || agentIds.length === 0) return rows; + const spendByAgentId = await getMonthlySpendByAgentIds(companyId, agentIds); + return rows.map((row) => ({ + ...row, + spentMonthlyCents: spendByAgentId.get(row.id) ?? 0, + })); + } + async function getById(id: string) { const row = await db .select() .from(agents) .where(eq(agents.id, id)) .then((rows) => rows[0] ?? null); - return row ? normalizeAgentRow(row) : null; + if (!row) return null; + const [hydrated] = await hydrateAgentSpend([row]); + return normalizeAgentRow(hydrated); } async function ensureManager(companyId: string, managerId: string) { @@ -331,7 +375,8 @@ export function agentService(db: Db) { conditions.push(ne(agents.status, "terminated")); } const rows = await db.select().from(agents).where(and(...conditions)); - return rows.map(normalizeAgentRow); + const hydrated = await hydrateAgentSpend(rows); + return hydrated.map(normalizeAgentRow); }, getById, @@ -360,14 +405,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 +434,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 +452,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..3e7d8b4e --- /dev/null +++ b/server/src/services/budgets.ts @@ -0,0 +1,958 @@ +import { and, desc, eq, gte, inArray, lt, ne, 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; + +export type BudgetEnforcementScope = { + companyId: string; + scopeType: BudgetScopeType; + scopeId: string; +}; + +export type BudgetServiceHooks = { + cancelWorkForScope?: (scope: BudgetEnforcementScope) => Promise; +}; + +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, + pauseReason: companies.pauseReason, + pausedAt: companies.pausedAt, + }) + .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" || Boolean(row.pausedAt), + pauseReason: (row.pauseReason as ScopeRecord["pauseReason"]) ?? 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, hooks: BudgetServiceHooks = {}) { + 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", + pauseReason: "budget", + pausedAt: now, + updatedAt: now, + }) + .where(eq(companies.id, policy.scopeId)); + } + + async function pauseAndCancelScopeForBudget(policy: PolicyRow) { + await pauseScopeForBudget(policy); + await hooks.cancelWorkForScope?.({ + companyId: policy.companyId, + scopeType: policy.scopeType as BudgetScopeType, + scopeId: 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", + pauseReason: null, + pausedAt: null, + updatedAt: now, + }) + .where(and(eq(companies.id, policy.scopeId), eq(companies.pauseReason, "budget"))); + } + + 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), + ne(budgetIncidents.status, "dismissed"), + ), + ) + .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 pauseAndCancelScopeForBudget(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 pauseAndCancelScopeForBudget(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, + pauseReason: companies.pauseReason, + 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.pauseReason === "budget" + ? "Company is paused because its budget hard-stop was reached." + : "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)); + const currentObserved = await computeObservedAmount(db, policy); + if (nextAmount <= currentObserved) { + throw unprocessable("New budget must exceed current observed spend"); + } + + const now = new Date(); + await db + .update(budgetPolicies) + .set({ + amount: nextAmount, + isActive: true, + updatedByUserId: actorUserId, + updatedAt: now, + }) + .where(eq(budgetPolicies.id, policy.id)); + + if (policy.scopeType === "company" && policy.windowKind === "calendar_month_utc") { + await db + .update(companies) + .set({ budgetMonthlyCents: nextAmount, updatedAt: now }) + .where(eq(companies.id, policy.scopeId)); + } + + if (policy.scopeType === "agent" && policy.windowKind === "calendar_month_utc") { + await db + .update(agents) + .set({ budgetMonthlyCents: nextAmount, updatedAt: now }) + .where(eq(agents.id, policy.scopeId)); + } + + await resumeScopeFromBudget(policy); + await db + .update(budgetIncidents) + .set({ + status: "resolved", + resolvedAt: now, + updatedAt: now, + }) + .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 38a1f12f..893bea9e 100644 --- a/server/src/services/companies.ts +++ b/server/src/services/companies.ts @@ -1,7 +1,9 @@ -import { eq, count } from "drizzle-orm"; +import { and, count, eq, gte, inArray, lt, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { companies, + companyLogos, + assets, agents, agentApiKeys, agentRuntimeState, @@ -14,6 +16,7 @@ import { heartbeatRuns, heartbeatRunEvents, costEvents, + financeEvents, approvalComments, approvals, activityLog, @@ -23,10 +26,84 @@ import { principalPermissionGrants, companyMemberships, } from "@paperclipai/db"; +import { notFound, unprocessable } from "../errors.js"; export function companyService(db: Db) { const ISSUE_PREFIX_FALLBACK = "CMP"; + const companySelection = { + id: companies.id, + name: companies.name, + description: companies.description, + status: companies.status, + issuePrefix: companies.issuePrefix, + issueCounter: companies.issueCounter, + budgetMonthlyCents: companies.budgetMonthlyCents, + spentMonthlyCents: companies.spentMonthlyCents, + requireBoardApprovalForNewAgents: companies.requireBoardApprovalForNewAgents, + brandColor: companies.brandColor, + logoAssetId: companyLogos.assetId, + createdAt: companies.createdAt, + updatedAt: companies.updatedAt, + }; + + function enrichCompany(company: T) { + return { + ...company, + logoUrl: company.logoAssetId ? `/api/assets/${company.logoAssetId}/content` : null, + }; + } + + function currentUtcMonthWindow(now = new Date()) { + const year = now.getUTCFullYear(); + const month = now.getUTCMonth(); + return { + start: new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)), + end: new Date(Date.UTC(year, month + 1, 1, 0, 0, 0, 0)), + }; + } + + async function getMonthlySpendByCompanyIds( + companyIds: string[], + database: Pick = db, + ) { + if (companyIds.length === 0) return new Map(); + const { start, end } = currentUtcMonthWindow(); + const rows = await database + .select({ + companyId: costEvents.companyId, + spentMonthlyCents: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, + }) + .from(costEvents) + .where( + and( + inArray(costEvents.companyId, companyIds), + gte(costEvents.occurredAt, start), + lt(costEvents.occurredAt, end), + ), + ) + .groupBy(costEvents.companyId); + return new Map(rows.map((row) => [row.companyId, Number(row.spentMonthlyCents ?? 0)])); + } + + async function hydrateCompanySpend( + rows: T[], + database: Pick = db, + ) { + const spendByCompanyId = await getMonthlySpendByCompanyIds(rows.map((row) => row.id), database); + return rows.map((row) => ({ + ...row, + spentMonthlyCents: spendByCompanyId.get(row.id) ?? 0, + })); + } + + function getCompanyQuery(database: Pick) { + return database + .select(companySelection) + .from(companies) + .leftJoin(companyLogos, eq(companyLogos.companyId, companies.id)); + } + function deriveIssuePrefixBase(name: string) { const normalized = name.toUpperCase().replace(/[^A-Z]/g, ""); return normalized.slice(0, 3) || ISSUE_PREFIX_FALLBACK; @@ -70,32 +147,109 @@ export function companyService(db: Db) { } return { - list: () => db.select().from(companies), + list: async () => { + const rows = await getCompanyQuery(db); + const hydrated = await hydrateCompanySpend(rows); + return hydrated.map((row) => enrichCompany(row)); + }, - getById: (id: string) => - db - .select() - .from(companies) + getById: async (id: string) => { + const row = await getCompanyQuery(db) .where(eq(companies.id, id)) - .then((rows) => rows[0] ?? null), + .then((rows) => rows[0] ?? null); + if (!row) return null; + const [hydrated] = await hydrateCompanySpend([row], db); + return enrichCompany(hydrated); + }, - create: async (data: typeof companies.$inferInsert) => createCompanyWithUniquePrefix(data), + create: async (data: typeof companies.$inferInsert) => { + const created = await createCompanyWithUniquePrefix(data); + const row = await getCompanyQuery(db) + .where(eq(companies.id, created.id)) + .then((rows) => rows[0] ?? null); + if (!row) throw notFound("Company not found after creation"); + const [hydrated] = await hydrateCompanySpend([row], db); + return enrichCompany(hydrated); + }, - update: (id: string, data: Partial) => - db - .update(companies) - .set({ ...data, updatedAt: new Date() }) - .where(eq(companies.id, id)) - .returning() - .then((rows) => rows[0] ?? null), + update: ( + id: string, + data: Partial & { logoAssetId?: string | null }, + ) => + db.transaction(async (tx) => { + const existing = await getCompanyQuery(tx) + .where(eq(companies.id, id)) + .then((rows) => rows[0] ?? null); + if (!existing) return null; + + const { logoAssetId, ...companyPatch } = data; + + if (logoAssetId !== undefined && logoAssetId !== null) { + const nextLogoAsset = await tx + .select({ id: assets.id, companyId: assets.companyId }) + .from(assets) + .where(eq(assets.id, logoAssetId)) + .then((rows) => rows[0] ?? null); + if (!nextLogoAsset) throw notFound("Logo asset not found"); + if (nextLogoAsset.companyId !== existing.id) { + throw unprocessable("Logo asset must belong to the same company"); + } + } + + const updated = await tx + .update(companies) + .set({ ...companyPatch, updatedAt: new Date() }) + .where(eq(companies.id, id)) + .returning() + .then((rows) => rows[0] ?? null); + if (!updated) return null; + + if (logoAssetId === null) { + await tx.delete(companyLogos).where(eq(companyLogos.companyId, id)); + } else if (logoAssetId !== undefined) { + await tx + .insert(companyLogos) + .values({ + companyId: id, + assetId: logoAssetId, + }) + .onConflictDoUpdate({ + target: companyLogos.companyId, + set: { + assetId: logoAssetId, + updatedAt: new Date(), + }, + }); + } + + if (logoAssetId !== undefined && existing.logoAssetId && existing.logoAssetId !== logoAssetId) { + await tx.delete(assets).where(eq(assets.id, existing.logoAssetId)); + } + + const [hydrated] = await hydrateCompanySpend([{ + ...updated, + logoAssetId: logoAssetId === undefined ? existing.logoAssetId : logoAssetId, + }], tx); + + return enrichCompany(hydrated); + }), archive: (id: string) => - db - .update(companies) - .set({ status: "archived", updatedAt: new Date() }) - .where(eq(companies.id, id)) - .returning() - .then((rows) => rows[0] ?? null), + db.transaction(async (tx) => { + const updated = await tx + .update(companies) + .set({ status: "archived", updatedAt: new Date() }) + .where(eq(companies.id, id)) + .returning() + .then((rows) => rows[0] ?? null); + if (!updated) return null; + const row = await getCompanyQuery(tx) + .where(eq(companies.id, id)) + .then((rows) => rows[0] ?? null); + if (!row) return null; + const [hydrated] = await hydrateCompanySpend([row], tx); + return enrichCompany(hydrated); + }), remove: (id: string) => db.transaction(async (tx) => { @@ -108,6 +262,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)); @@ -116,6 +271,8 @@ export function companyService(db: Db) { await tx.delete(principalPermissionGrants).where(eq(principalPermissionGrants.companyId, id)); await tx.delete(companyMemberships).where(eq(companyMemberships.companyId, id)); await tx.delete(issues).where(eq(issues.companyId, id)); + await tx.delete(companyLogos).where(eq(companyLogos.companyId, id)); + await tx.delete(assets).where(eq(assets.companyId, id)); await tx.delete(goals).where(eq(goals.companyId, id)); await tx.delete(projects).where(eq(projects.companyId, id)); await tx.delete(agents).where(eq(agents.companyId, id)); diff --git a/server/src/services/costs.ts b/server/src/services/costs.ts index 2d430aa9..76a90f2d 100644 --- a/server/src/services/costs.ts +++ b/server/src/services/costs.ts @@ -1,14 +1,50 @@ -import { and, desc, eq, gte, isNotNull, lte, sql } from "drizzle-orm"; +import { and, desc, eq, gte, isNotNull, lt, 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, type BudgetServiceHooks } from "./budgets.js"; export interface CostDateRange { from?: Date; to?: Date; } -export function costService(db: Db) { +const METERED_BILLING_TYPE = "metered_api"; +const SUBSCRIPTION_BILLING_TYPES = ["subscription_included", "subscription_overage"] as const; + +function currentUtcMonthWindow(now = new Date()) { + const year = now.getUTCFullYear(); + const month = now.getUTCMonth(); + return { + start: new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)), + end: new Date(Date.UTC(year, month + 1, 1, 0, 0, 0, 0)), + }; +} + +async function getMonthlySpendTotal( + db: Db, + scope: { companyId: string; agentId?: string | null }, +) { + const { start, end } = currentUtcMonthWindow(); + const conditions = [ + eq(costEvents.companyId, scope.companyId), + gte(costEvents.occurredAt, start), + lt(costEvents.occurredAt, end), + ]; + if (scope.agentId) { + conditions.push(eq(costEvents.agentId, scope.agentId)); + } + const [row] = await db + .select({ + total: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, + }) + .from(costEvents) + .where(and(...conditions)); + return Number(row?.total ?? 0); +} + +export function costService(db: Db, budgetHooks: BudgetServiceHooks = {}) { + const budgets = budgetService(db, budgetHooks); return { createEvent: async (companyId: string, data: Omit) => { const agent = await db @@ -24,14 +60,25 @@ 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]); + const [agentMonthSpend, companyMonthSpend] = await Promise.all([ + getMonthlySpendTotal(db, { companyId, agentId: event.agentId }), + getMonthlySpendTotal(db, { companyId }), + ]); + await db .update(agents) .set({ - spentMonthlyCents: sql`${agents.spentMonthlyCents} + ${event.costCents}`, + spentMonthlyCents: agentMonthSpend, updatedAt: new Date(), }) .where(eq(agents.id, event.agentId)); @@ -39,29 +86,12 @@ export function costService(db: Db) { await db .update(companies) .set({ - spentMonthlyCents: sql`${companies.spentMonthlyCents} + ${event.costCents}`, + spentMonthlyCents: companyMonthSpend, updatedAt: new Date(), }) .where(eq(companies.id, companyId)); - const updatedAgent = await db - .select() - .from(agents) - .where(eq(agents.id, event.agentId)) - .then((rows) => rows[0] ?? null); - - if ( - updatedAgent && - updatedAgent.budgetMonthlyCents > 0 && - updatedAgent.spentMonthlyCents >= updatedAgent.budgetMonthlyCents && - updatedAgent.status !== "paused" && - updatedAgent.status !== "terminated" - ) { - await db - .update(agents) - .set({ status: "paused", updatedAt: new Date() }) - .where(eq(agents.id, updatedAgent.id)); - } + await budgets.evaluateCostEvent(event); return event; }, @@ -105,52 +135,180 @@ 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.finishedAt, range.from)); - if (range?.to) runConditions.push(lte(heartbeatRuns.finishedAt, range.to)); + byProvider: 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, + 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`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`, }) - .from(heartbeatRuns) - .where(and(...runConditions)) - .groupBy(heartbeatRuns.agentId); + .from(costEvents) + .where(and(...conditions)) + .groupBy(costEvents.provider, costEvents.biller, costEvents.billingType, costEvents.model) + .orderBy(desc(sql`coalesce(sum(${costEvents.costCents}), 0)::int`)); + }, - 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, - }; - }); + 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)); + + return db + .select({ + 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`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`, + providerCount: sql`count(distinct ${costEvents.provider})::int`, + modelCount: sql`count(distinct ${costEvents.model})::int`, + }) + .from(costEvents) + .where(and(...conditions)) + .groupBy(costEvents.biller) + .orderBy(desc(sql`coalesce(sum(${costEvents.costCents}), 0)::int`)); + }, + + /** + * aggregates cost_events by provider for each of three rolling windows: + * last 5 hours, last 24 hours, last 7 days. + * purely internal consumption data, no external rate-limit sources. + */ + windowSpend: async (companyId: string) => { + const windows = [ + { label: "5h", hours: 5 }, + { label: "24h", hours: 24 }, + { label: "7d", hours: 168 }, + ] as const; + + const results = await Promise.all( + windows.map(async ({ label, hours }) => { + const since = new Date(Date.now() - hours * 60 * 60 * 1000); + 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) + .where( + and( + eq(costEvents.companyId, companyId), + gte(costEvents.occurredAt, since), + ), + ) + .groupBy(costEvents.provider) + .orderBy(desc(sql`coalesce(sum(${costEvents.costCents}), 0)::int`)); + + 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, + })); + }), + ); + + return results.flat(); + }, + + byAgentModel: 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)); + + // single query: group by agent + provider + model. + // the (companyId, agentId, occurredAt) composite index covers this well. + // order by provider + model for stable db-level ordering; cost-desc sort + // within each agent's sub-rows is done client-side in the ui memo. + return db + .select({ + 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.biller, + costEvents.billingType, + costEvents.model, + ) + .orderBy(costEvents.provider, costEvents.biller, costEvents.billingType, costEvents.model); }, byProject: async (companyId: string, range?: CostDateRange) => { @@ -179,25 +337,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.finishedAt, range.from)); - if (range?.to) conditions.push(lte(heartbeatRuns.finishedAt, 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 3f751e7a..a7758e39 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, @@ -23,6 +24,7 @@ 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 { companySkillService } from "./company-skills.js"; +import { budgetService, type BudgetEnforcementScope } from "./budgets.js"; import { secretService } from "./secrets.js"; import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js"; import { summarizeHeartbeatRunResultJson } from "./heartbeat-run-summary.js"; @@ -171,6 +173,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 { @@ -559,6 +622,10 @@ export function heartbeatService(db: Db) { const companySkills = companySkillService(db); const issuesSvc = issueService(db); const activeRunExecutions = new Set(); + const budgetHooks = { + cancelWorkForScope: cancelBudgetScopeWork, + }; + const budgets = budgetService(db, budgetHooks); async function getAgent(agentId: string) { return db @@ -1141,6 +1208,26 @@ export function heartbeatService(db: Db) { async function claimQueuedRun(run: typeof heartbeatRuns.$inferSelect) { if (run.status !== "queued") return run; + const agent = await getAgent(run.agentId); + if (!agent) { + await cancelRunInternal(run.id, "Cancelled because the agent no longer exists"); + return null; + } + if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") { + await cancelRunInternal(run.id, "Cancelled because the agent is not invokable"); + return null; + } + + const context = parseObject(run.contextSnapshot); + const budgetBlock = await budgets.getInvocationBlock(run.companyId, run.agentId, { + issueId: readNonEmptyString(context.issueId), + projectId: readNonEmptyString(context.projectId), + }); + if (budgetBlock) { + await cancelRunInternal(run.id, budgetBlock.reason); + return null; + } + const claimedAt = new Date(); const claimed = await db .update(heartbeatRuns) @@ -1296,8 +1383,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,12 +1407,18 @@ export function heartbeatService(db: Db) { .where(eq(agentRuntimeState.agentId, agent.id)); if (additionalCostCents > 0 || hasTokenUsage) { - const costs = costService(db); + const costs = costService(db, budgetHooks); 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(), @@ -1333,6 +1430,9 @@ export function heartbeatService(db: Db) { return withAgentStartLock(agentId, async () => { const agent = await getAgent(agentId); if (!agent) return []; + if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") { + return []; + } const policy = parseHeartbeatPolicy(agent); const runningCount = await countRunningRunsForAgent(agentId); const availableSlots = Math.max(0, policy.maxConcurrentRuns - runningCount); @@ -1882,8 +1982,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; @@ -2233,6 +2336,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" || @@ -2242,21 +2382,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"); @@ -2666,6 +2791,205 @@ export function heartbeatService(db: Db) { return newRun; } + async function listProjectScopedRunIds(companyId: string, projectId: string) { + const runIssueId = sql`${heartbeatRuns.contextSnapshot} ->> 'issueId'`; + const effectiveProjectId = sql`coalesce(${heartbeatRuns.contextSnapshot} ->> 'projectId', ${issues.projectId}::text)`; + + const rows = await db + .selectDistinctOn([heartbeatRuns.id], { id: heartbeatRuns.id }) + .from(heartbeatRuns) + .leftJoin( + issues, + and( + eq(issues.companyId, companyId), + sql`${issues.id}::text = ${runIssueId}`, + ), + ) + .where( + and( + eq(heartbeatRuns.companyId, companyId), + inArray(heartbeatRuns.status, ["queued", "running"]), + sql`${effectiveProjectId} = ${projectId}`, + ), + ); + + return rows.map((row) => row.id); + } + + async function listProjectScopedWakeupIds(companyId: string, projectId: string) { + const wakeIssueId = sql`${agentWakeupRequests.payload} ->> 'issueId'`; + const effectiveProjectId = sql`coalesce(${agentWakeupRequests.payload} ->> 'projectId', ${issues.projectId}::text)`; + + const rows = await db + .selectDistinctOn([agentWakeupRequests.id], { id: agentWakeupRequests.id }) + .from(agentWakeupRequests) + .leftJoin( + issues, + and( + eq(issues.companyId, companyId), + sql`${issues.id}::text = ${wakeIssueId}`, + ), + ) + .where( + and( + eq(agentWakeupRequests.companyId, companyId), + inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), + sql`${agentWakeupRequests.runId} is null`, + sql`${effectiveProjectId} = ${projectId}`, + ), + ); + + return rows.map((row) => row.id); + } + + async function cancelPendingWakeupsForBudgetScope(scope: BudgetEnforcementScope) { + const now = new Date(); + let wakeupIds: string[] = []; + + if (scope.scopeType === "company") { + wakeupIds = await db + .select({ id: agentWakeupRequests.id }) + .from(agentWakeupRequests) + .where( + and( + eq(agentWakeupRequests.companyId, scope.companyId), + inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), + sql`${agentWakeupRequests.runId} is null`, + ), + ) + .then((rows) => rows.map((row) => row.id)); + } else if (scope.scopeType === "agent") { + wakeupIds = await db + .select({ id: agentWakeupRequests.id }) + .from(agentWakeupRequests) + .where( + and( + eq(agentWakeupRequests.companyId, scope.companyId), + eq(agentWakeupRequests.agentId, scope.scopeId), + inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), + sql`${agentWakeupRequests.runId} is null`, + ), + ) + .then((rows) => rows.map((row) => row.id)); + } else { + wakeupIds = await listProjectScopedWakeupIds(scope.companyId, scope.scopeId); + } + + if (wakeupIds.length === 0) return 0; + + await db + .update(agentWakeupRequests) + .set({ + status: "cancelled", + finishedAt: now, + error: "Cancelled due to budget pause", + updatedAt: now, + }) + .where(inArray(agentWakeupRequests.id, wakeupIds)); + + return wakeupIds.length; + } + + async function cancelRunInternal(runId: string, reason = "Cancelled by control plane") { + const run = await getRun(runId); + if (!run) throw notFound("Heartbeat run not found"); + if (run.status !== "running" && run.status !== "queued") return run; + + const running = runningProcesses.get(run.id); + if (running) { + running.child.kill("SIGTERM"); + const graceMs = Math.max(1, running.graceSec) * 1000; + setTimeout(() => { + if (!running.child.killed) { + running.child.kill("SIGKILL"); + } + }, graceMs); + } + + const cancelled = await setRunStatus(run.id, "cancelled", { + finishedAt: new Date(), + error: reason, + errorCode: "cancelled", + }); + + await setWakeupStatus(run.wakeupRequestId, "cancelled", { + finishedAt: new Date(), + error: reason, + }); + + if (cancelled) { + await appendRunEvent(cancelled, 1, { + eventType: "lifecycle", + stream: "system", + level: "warn", + message: "run cancelled", + }); + await releaseIssueExecutionAndPromote(cancelled); + } + + runningProcesses.delete(run.id); + await finalizeAgentStatus(run.agentId, "cancelled"); + await startNextQueuedRunForAgent(run.agentId); + return cancelled; + } + + async function cancelActiveForAgentInternal(agentId: string, reason = "Cancelled due to agent pause") { + const runs = await db + .select() + .from(heartbeatRuns) + .where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, ["queued", "running"]))); + + for (const run of runs) { + await setRunStatus(run.id, "cancelled", { + finishedAt: new Date(), + error: reason, + errorCode: "cancelled", + }); + + await setWakeupStatus(run.wakeupRequestId, "cancelled", { + finishedAt: new Date(), + error: reason, + }); + + const running = runningProcesses.get(run.id); + if (running) { + running.child.kill("SIGTERM"); + runningProcesses.delete(run.id); + } + await releaseIssueExecutionAndPromote(run); + } + + return runs.length; + } + + async function cancelBudgetScopeWork(scope: BudgetEnforcementScope) { + if (scope.scopeType === "agent") { + await cancelActiveForAgentInternal(scope.scopeId, "Cancelled due to budget pause"); + await cancelPendingWakeupsForBudgetScope(scope); + return; + } + + const runIds = + scope.scopeType === "company" + ? await db + .select({ id: heartbeatRuns.id }) + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.companyId, scope.companyId), + inArray(heartbeatRuns.status, ["queued", "running"]), + ), + ) + .then((rows) => rows.map((row) => row.id)) + : await listProjectScopedRunIds(scope.companyId, scope.scopeId); + + for (const runId of runIds) { + await cancelRunInternal(runId, "Cancelled due to budget pause"); + } + + await cancelPendingWakeupsForBudgetScope(scope); + } + return { list: async (companyId: string, agentId?: string, limit?: number) => { const query = db @@ -2838,77 +3162,11 @@ export function heartbeatService(db: Db) { return { checked, enqueued, skipped }; }, - cancelRun: async (runId: string) => { - const run = await getRun(runId); - if (!run) throw notFound("Heartbeat run not found"); - if (run.status !== "running" && run.status !== "queued") return run; + cancelRun: (runId: string) => cancelRunInternal(runId), - const running = runningProcesses.get(run.id); - if (running) { - running.child.kill("SIGTERM"); - const graceMs = Math.max(1, running.graceSec) * 1000; - setTimeout(() => { - if (!running.child.killed) { - running.child.kill("SIGKILL"); - } - }, graceMs); - } + cancelActiveForAgent: (agentId: string) => cancelActiveForAgentInternal(agentId), - const cancelled = await setRunStatus(run.id, "cancelled", { - finishedAt: new Date(), - error: "Cancelled by control plane", - errorCode: "cancelled", - }); - - await setWakeupStatus(run.wakeupRequestId, "cancelled", { - finishedAt: new Date(), - error: "Cancelled by control plane", - }); - - if (cancelled) { - await appendRunEvent(cancelled, 1, { - eventType: "lifecycle", - stream: "system", - level: "warn", - message: "run cancelled", - }); - await releaseIssueExecutionAndPromote(cancelled); - } - - runningProcesses.delete(run.id); - await finalizeAgentStatus(run.agentId, "cancelled"); - await startNextQueuedRunForAgent(run.agentId); - return cancelled; - }, - - cancelActiveForAgent: async (agentId: string) => { - const runs = await db - .select() - .from(heartbeatRuns) - .where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, ["queued", "running"]))); - - for (const run of runs) { - await setRunStatus(run.id, "cancelled", { - finishedAt: new Date(), - error: "Cancelled due to agent pause", - errorCode: "cancelled", - }); - - await setWakeupStatus(run.wakeupRequestId, "cancelled", { - finishedAt: new Date(), - error: "Cancelled due to agent pause", - }); - - const running = runningProcesses.get(run.id); - if (running) { - running.child.kill("SIGTERM"); - runningProcesses.delete(run.id); - } - await releaseIssueExecutionAndPromote(run); - } - - return runs.length; - }, + cancelBudgetScopeWork, getActiveRunForAgent: async (agentId: string) => { const [run] = await db diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 332de85c..568a1c7d 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -9,8 +9,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 new file mode 100644 index 00000000..664406ab --- /dev/null +++ b/server/src/services/quota-windows.ts @@ -0,0 +1,64 @@ +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. + * Individual adapter failures are caught and returned as error results rather than + * letting one provider's outage block the entire response. + */ +export async function fetchAllQuotaWindows(): Promise { + const adapters = listServerAdapters().filter((a) => a.getQuotaWindows != null); + + const settled = await Promise.allSettled( + adapters.map((adapter) => withQuotaTimeout(adapter.type, adapter.getQuotaWindows!())), + ); + + return settled.map((result, i) => { + if (result.status === "fulfilled") return result.value; + const adapterType = adapters[i]!.type; + return { + 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/App.tsx b/ui/src/App.tsx index 4223e0aa..d38de4b0 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -139,6 +139,7 @@ function boardRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/api/assets.ts b/ui/src/api/assets.ts index 8b3d056c..6fcf323f 100644 --- a/ui/src/api/assets.ts +++ b/ui/src/api/assets.ts @@ -11,11 +11,19 @@ export const assetsApi = { const safeFile = new File([buffer], file.name, { type: file.type }); const form = new FormData(); - form.append("file", safeFile); if (namespace && namespace.trim().length > 0) { form.append("namespace", namespace.trim()); } + form.append("file", safeFile); return api.postForm(`/companies/${companyId}/assets/images`, form); }, -}; + uploadCompanyLogo: async (companyId: string, file: File) => { + const buffer = await file.arrayBuffer(); + const safeFile = new File([buffer], file.name, { type: file.type }); + + const form = new FormData(); + form.append("file", safeFile); + return api.postForm(`/companies/${companyId}/logo`, form); + }, +}; 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/companies.ts b/ui/src/api/companies.ts index 69a1bf0c..d048e756 100644 --- a/ui/src/api/companies.ts +++ b/ui/src/api/companies.ts @@ -14,14 +14,18 @@ export const companiesApi = { list: () => api.get("/companies"), get: (companyId: string) => api.get(`/companies/${companyId}`), stats: () => api.get("/companies/stats"), - create: (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) => + create: (data: { + name: string; + description?: string | null; + budgetMonthlyCents?: number; + }) => api.post("/companies", data), update: ( companyId: string, data: Partial< Pick< Company, - "name" | "description" | "status" | "budgetMonthlyCents" | "requireBoardApprovalForNewAgents" | "brandColor" + "name" | "description" | "status" | "budgetMonthlyCents" | "requireBoardApprovalForNewAgents" | "brandColor" | "logoAssetId" > >, ) => api.patch(`/companies/${companyId}`, data), diff --git a/ui/src/api/costs.ts b/ui/src/api/costs.ts index 2bfa2ecb..4dda60a7 100644 --- a/ui/src/api/costs.ts +++ b/ui/src/api/costs.ts @@ -1,14 +1,19 @@ -import type { CostSummary, CostByAgent } from "@paperclipai/shared"; +import type { + CostSummary, + CostByAgent, + CostByProviderModel, + CostByBiller, + CostByAgentModel, + CostByProject, + CostWindowSpendRow, + FinanceSummary, + FinanceByBiller, + FinanceByKind, + FinanceEvent, + ProviderQuotaResult, +} from "@paperclipai/shared"; import { api } from "./client"; -export interface CostByProject { - projectId: string | null; - projectName: string | null; - costCents: number; - inputTokens: number; - outputTokens: number; -} - function dateParams(from?: string, to?: string): string { const params = new URLSearchParams(); if (from) params.set("from", from); @@ -22,6 +27,33 @@ export const costsApi = { api.get(`/companies/${companyId}/costs/summary${dateParams(from, to)}`), byAgent: (companyId: string, from?: string, to?: string) => api.get(`/companies/${companyId}/costs/by-agent${dateParams(from, to)}`), + byAgentModel: (companyId: string, from?: string, to?: string) => + api.get(`/companies/${companyId}/costs/by-agent-model${dateParams(from, to)}`), byProject: (companyId: string, from?: string, to?: string) => 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..7834e8cb --- /dev/null +++ b/ui/src/components/BudgetPolicyCard.tsx @@ -0,0 +1,219 @@ +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, + variant = "card", +}: { + summary: BudgetPolicySummary; + onSave?: (amountCents: number) => void; + isSaving?: boolean; + compact?: boolean; + variant?: "card" | "plain"; +}) { + 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; + const isPlain = variant === "plain"; + + const observedBudgetGrid = isPlain ? ( +
+
+
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` : ""} +
+
+
+ ) : ( +
+
+
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` : ""} +
+
+
+ ); + + const progressSection = ( +
+
+ Remaining + {summary.amount > 0 ? formatCents(summary.remainingAmount) : "Unlimited"} +
+
+
+
+
+ ); + + const pausedPane = 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; + + const saveSection = onSave ? ( +
+
+ + setDraftBudget(event.target.value)} + className="mt-2" + inputMode="decimal" + placeholder="0.00" + /> +
+ +
+ ) : null; + + if (isPlain) { + return ( +
+
+
+
+ {summary.scopeType} +
+
{summary.scopeName}
+
{windowLabel(summary.windowKind)}
+
+
+ + {summary.paused ? "Paused" : summary.status === "warning" ? "Warning" : summary.status === "hard_stop" ? "Hard stop" : "Healthy"} +
+
+ + {observedBudgetGrid} + {progressSection} + {pausedPane} + {saveSection} + {parsedDraft === null ? ( +

Enter a valid non-negative dollar amount.

+ ) : null} +
+ ); + } + + return ( + + +
+
+
+ {summary.scopeType} +
+ {summary.scopeName} + {windowLabel(summary.windowKind)} +
+
+ + {summary.paused ? "Paused" : summary.status === "warning" ? "Warning" : summary.status === "hard_stop" ? "Hard stop" : "Healthy"} +
+
+
+ + {observedBudgetGrid} + {progressSection} + {pausedPane} + {saveSection} + {parsedDraft === null ? ( +

Enter a valid non-negative dollar amount.

+ ) : null} +
+
+ ); +} diff --git a/ui/src/components/BudgetSidebarMarker.tsx b/ui/src/components/BudgetSidebarMarker.tsx new file mode 100644 index 00000000..43f10b95 --- /dev/null +++ b/ui/src/components/BudgetSidebarMarker.tsx @@ -0,0 +1,13 @@ +import { DollarSign } from "lucide-react"; + +export function BudgetSidebarMarker({ title = "Paused by budget" }: { title?: string }) { + return ( + + + + ); +} 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/CompanyPatternIcon.tsx b/ui/src/components/CompanyPatternIcon.tsx index c7e5acc3..6ea40788 100644 --- a/ui/src/components/CompanyPatternIcon.tsx +++ b/ui/src/components/CompanyPatternIcon.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { cn } from "../lib/utils"; const BAYER_4X4 = [ @@ -10,6 +10,7 @@ const BAYER_4X4 = [ interface CompanyPatternIconProps { companyName: string; + logoUrl?: string | null; brandColor?: string | null; className?: string; } @@ -159,8 +160,18 @@ function makeCompanyPatternDataUrl(seed: string, brandColor?: string | null, log return canvas.toDataURL("image/png"); } -export function CompanyPatternIcon({ companyName, brandColor, className }: CompanyPatternIconProps) { +export function CompanyPatternIcon({ + companyName, + logoUrl, + brandColor, + className, +}: CompanyPatternIconProps) { const initial = companyName.trim().charAt(0).toUpperCase() || "?"; + const [imageError, setImageError] = useState(false); + const logo = !imageError && typeof logoUrl === "string" && logoUrl.trim().length > 0 ? logoUrl : null; + useEffect(() => { + setImageError(false); + }, [logoUrl]); const patternDataUrl = useMemo( () => makeCompanyPatternDataUrl(companyName.trim().toLowerCase(), brandColor), [companyName, brandColor], @@ -173,7 +184,14 @@ export function CompanyPatternIcon({ companyName, brandColor, className }: Compa className, )} > - {patternDataUrl ? ( + {logo ? ( + {`${companyName} setImageError(true)} + className="absolute inset-0 h-full w-full object-cover" + /> + ) : patternDataUrl ? ( )} - - {initial} - + {!logo && ( + + {initial} + + )}
); } diff --git a/ui/src/components/CompanyRail.tsx b/ui/src/components/CompanyRail.tsx index fa981d1b..2ec8fc06 100644 --- a/ui/src/components/CompanyRail.tsx +++ b/ui/src/components/CompanyRail.tsx @@ -122,6 +122,7 @@ function SortableCompanyItem({ > + +
+
+ {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 new file mode 100644 index 00000000..21800bc1 --- /dev/null +++ b/ui/src/components/ProviderQuotaCard.tsx @@ -0,0 +1,416 @@ +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 { 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; + +interface ProviderQuotaCardProps { + provider: string; + rows: CostByProviderModel[]; + /** company monthly budget in cents (0 means unlimited) */ + budgetMonthlyCents: number; + /** total company spend in this period in cents, all providers */ + totalCompanySpendCents: number; + /** spend in the current calendar week in cents, this provider only */ + weekSpendCents: number; + /** rolling window rows for this provider: 5h, 24h, 7d */ + windowRows: CostWindowSpendRow[]; + 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({ + provider, + rows, + budgetMonthlyCents, + totalCompanySpendCents, + weekSpendCents, + 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 + // card is mounted twice: once in the "all" tab grid and once in its per-provider tab). + const totals = useMemo(() => { + let inputTokens = 0, outputTokens = 0, costCents = 0; + let apiRunCount = 0, subRunCount = 0, subInputTokens = 0, subOutputTokens = 0; + for (const r of rows) { + inputTokens += r.inputTokens; + outputTokens += r.outputTokens; + costCents += r.costCents; + apiRunCount += r.apiRunCount; + subRunCount += r.subscriptionRunCount; + subInputTokens += r.subscriptionInputTokens; + subOutputTokens += r.subscriptionOutputTokens; + } + const totalTokens = inputTokens + outputTokens; + const subTokens = subInputTokens + subOutputTokens; + // denominator: api-billed tokens (from cost_events) + subscription tokens (from heartbeat_runs) + const allTokens = totalTokens + subTokens; + return { + totalInputTokens: inputTokens, + totalOutputTokens: outputTokens, + totalTokens, + totalCostCents: costCents, + totalApiRuns: apiRunCount, + totalSubRuns: subRunCount, + totalSubInputTokens: subInputTokens, + totalSubOutputTokens: subOutputTokens, + totalSubTokens: subTokens, + subSharePct: allTokens > 0 ? (subTokens / allTokens) * 100 : 0, + }; + }, [rows]); + + const { + totalInputTokens, + totalOutputTokens, + totalTokens, + totalCostCents, + totalApiRuns, + totalSubRuns, + totalSubInputTokens, + totalSubOutputTokens, + totalSubTokens, + subSharePct, + } = totals; + + // budget bars: use this provider's own spend vs its pro-rata share of budget + // pro-rata: if a provider is 40% of total spend, it gets 40% of the budget allocated. + // falls back to raw provider spend vs total budget when totalCompanySpend is 0. + const providerBudgetShare = + budgetMonthlyCents > 0 && totalCompanySpendCents > 0 + ? (totalCostCents / totalCompanySpendCents) * budgetMonthlyCents + : budgetMonthlyCents; + + const budgetPct = + providerBudgetShare > 0 + ? Math.min(100, (totalCostCents / providerBudgetShare) * 100) + : 0; + + // 4.33 = average weeks per calendar month (52 / 12) + const weeklyBudgetShare = providerBudgetShare > 0 ? providerBudgetShare / 4.33 : 0; + const weekPct = + weeklyBudgetShare > 0 ? Math.min(100, (weekSpendCents / weeklyBudgetShare) * 100) : 0; + + const hasBudget = budgetMonthlyCents > 0; + + // memoized so the Map and max are not reconstructed on every parent render tick + const windowMap = useMemo( + () => new Map(windowRows.map((r) => [r.window, r])), + [windowRows], + ); + const maxWindowCents = useMemo( + () => 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 ( + + +
+
+ + {providerDisplayName(provider)} + + + {formatTokens(totalInputTokens)} in + {" · "} + {formatTokens(totalOutputTokens)} out + {(totalApiRuns > 0 || totalSubRuns > 0) && ( + + ·{" "} + {totalApiRuns > 0 && `~${totalApiRuns} api`} + {totalApiRuns > 0 && totalSubRuns > 0 && " / "} + {totalSubRuns > 0 && `~${totalSubRuns} sub`} + {" runs"} + + )} + +
+ + {formatCents(totalCostCents)} + +
+
+ + + {hasBudget && ( +
+ + = 100} + /> +
+ )} + + {/* rolling window consumption — always shown when data is available */} + {windowRows.length > 0 && ( + <> +
+
+

+ Rolling windows +

+
+ {ROLLING_WINDOWS.map((w) => { + const row = windowMap.get(w); + // omit windows with no data rather than showing false $0.00 zeros + if (!row) return null; + const cents = row.costCents; + const tokens = row.inputTokens + row.outputTokens; + const barPct = maxWindowCents > 0 ? (cents / maxWindowCents) * 100 : 0; + return ( +
+
+ {w} + + {formatTokens(tokens)} tok + + {formatCents(cents)} +
+
+
+
+
+ ); + })} +
+
+ + )} + + {/* subscription usage — shown when any subscription-billed runs exist */} + {totalSubRuns > 0 && ( + <> +
+
+

+ Subscription +

+

+ {totalSubRuns} runs + {" · "} + {totalSubTokens > 0 && ( + <> + {formatTokens(totalSubTokens)} total + {" · "} + + )} + {formatTokens(totalSubInputTokens)} in + {" · "} + {formatTokens(totalSubOutputTokens)} out +

+ {subSharePct > 0 && ( + <> +
+
+
+

+ {Math.round(subSharePct)}% of token usage via subscription +

+ + )} +
+ + )} + + {/* model breakdown — always shown, with token-share bars */} + {rows.length > 0 && ( + <> +
+
+ {rows.map((row) => { + const rowTokens = row.inputTokens + row.outputTokens; + const tokenPct = totalTokens > 0 ? (rowTokens / totalTokens) * 100 : 0; + const costPct = totalCostCents > 0 ? (row.costCents / totalCostCents) * 100 : 0; + return ( +
+ {/* model name and cost */} +
+
+ + {row.model} + + + {providerDisplayName(row.biller)} · {billingTypeDisplayName(row.billingType)} + +
+
+ + {formatTokens(rowTokens)} tok + + {formatCents(row.costCents)} +
+
+ {/* token share bar */} +
+
+ {/* cost share overlay — narrower, opaque, shows relative cost weight */} +
+
+
+ ); + })} +
+ + )} + + {/* 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/components/QuotaBar.tsx b/ui/src/components/QuotaBar.tsx new file mode 100644 index 00000000..89c25c6e --- /dev/null +++ b/ui/src/components/QuotaBar.tsx @@ -0,0 +1,65 @@ +import { cn } from "@/lib/utils"; + +interface QuotaBarProps { + label: string; + // value between 0 and 100 + percentUsed: number; + leftLabel: string; + rightLabel?: string; + // shows a 2px destructive notch at the fill tip when true + showDeficitNotch?: boolean; + className?: string; +} + +function fillColor(pct: number): string { + if (pct > 90) return "bg-red-400"; + if (pct > 70) return "bg-yellow-400"; + return "bg-green-400"; +} + +export function QuotaBar({ + label, + percentUsed, + leftLabel, + rightLabel, + showDeficitNotch = false, + className, +}: QuotaBarProps) { + const clampedPct = Math.min(100, Math.max(0, percentUsed)); + // keep the notch visible even near the edges + const notchLeft = Math.min(clampedPct, 97); + + return ( +
+ {/* row header */} +
+ {label} +
+ {leftLabel} + {rightLabel && ( + {rightLabel} + )} +
+
+ + {/* track — boxed border, square corners to match the theme */} +
+ {/* fill */} +
+ {/* deficit notch — 2px wide, sits at the fill tip */} + {showDeficitNotch && clampedPct > 0 && ( +
+ )} +
+
+ ); +} diff --git a/ui/src/components/SidebarAgents.tsx b/ui/src/components/SidebarAgents.tsx index b94ccfe4..5b438f0a 100644 --- a/ui/src/components/SidebarAgents.tsx +++ b/ui/src/components/SidebarAgents.tsx @@ -10,6 +10,7 @@ import { heartbeatsApi } from "../api/heartbeats"; import { queryKeys } from "../lib/queryKeys"; import { cn, agentRouteRef, agentUrl } from "../lib/utils"; import { AgentIcon } from "./AgentIconPicker"; +import { BudgetSidebarMarker } from "./BudgetSidebarMarker"; import { Collapsible, CollapsibleContent, @@ -124,15 +125,22 @@ export function SidebarAgents() { > {agent.name} - {runCount > 0 && ( + {(agent.pauseReason === "budget" || runCount > 0) && ( - - - - - - {runCount} live - + {agent.pauseReason === "budget" ? ( + + ) : null} + {runCount > 0 ? ( + + + + + ) : null} + {runCount > 0 ? ( + + {runCount} live + + ) : null} )} diff --git a/ui/src/components/SidebarProjects.tsx b/ui/src/components/SidebarProjects.tsx index 72d0a217..cc6f417c 100644 --- a/ui/src/components/SidebarProjects.tsx +++ b/ui/src/components/SidebarProjects.tsx @@ -20,6 +20,7 @@ import { projectsApi } from "../api/projects"; import { queryKeys } from "../lib/queryKeys"; import { cn, projectRouteRef } from "../lib/utils"; import { useProjectOrder } from "../hooks/useProjectOrder"; +import { BudgetSidebarMarker } from "./BudgetSidebarMarker"; import { Collapsible, CollapsibleContent, @@ -88,6 +89,7 @@ function SortableProjectItem({ style={{ backgroundColor: project.color ?? "#6366f1" }} /> {project.name} + {project.pauseReason === "budget" ? : null} {projectSidebarSlots.length > 0 && (
diff --git a/ui/src/context/CompanyContext.tsx b/ui/src/context/CompanyContext.tsx index fb074f33..86afa175 100644 --- a/ui/src/context/CompanyContext.tsx +++ b/ui/src/context/CompanyContext.tsx @@ -85,7 +85,11 @@ export function CompanyProvider({ children }: { children: ReactNode }) { }, [queryClient]); const createMutation = useMutation({ - mutationFn: (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) => + mutationFn: (data: { + name: string; + description?: string | null; + budgetMonthlyCents?: number; + }) => companiesApi.create(data), onSuccess: (company) => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); @@ -94,7 +98,11 @@ export function CompanyProvider({ children }: { children: ReactNode }) { }); const createCompany = useCallback( - async (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) => { + async (data: { + name: string; + description?: string | null; + budgetMonthlyCents?: number; + }) => { return createMutation.mutateAsync(data); }, [createMutation], diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index 96e3f654..5ad06a72 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -413,6 +413,10 @@ function invalidateActivityQueries( if (entityType === "cost_event") { queryClient.invalidateQueries({ queryKey: queryKeys.costs(companyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.usageByProvider(companyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.usageWindowSpend(companyId) }); + // usageQuotaWindows is intentionally excluded: quota windows come from external provider + // apis on a 5-minute poll and do not change in response to cost events logged by agents return; } diff --git a/ui/src/hooks/useDateRange.ts b/ui/src/hooks/useDateRange.ts new file mode 100644 index 00000000..2e4a9487 --- /dev/null +++ b/ui/src/hooks/useDateRange.ts @@ -0,0 +1,120 @@ +import { useEffect, useMemo, useRef, useState } from "react"; + +export type DatePreset = "mtd" | "7d" | "30d" | "ytd" | "all" | "custom"; + +export const PRESET_LABELS: Record = { + mtd: "Month to Date", + "7d": "Last 7 Days", + "30d": "Last 30 Days", + ytd: "Year to Date", + all: "All Time", + custom: "Custom", +}; + +export const PRESET_KEYS: DatePreset[] = ["mtd", "7d", "30d", "ytd", "all", "custom"]; + +// note: computeRange is called inside a useMemo that re-evaluates once per minute +// (driven by minuteTick). this means sliding windows (7d, 30d) advance their upper +// bound at most once per minute — acceptable for a cost dashboard. +function computeRange(preset: DatePreset): { from: string; to: string } { + const now = new Date(); + const to = now.toISOString(); + switch (preset) { + case "mtd": { + const d = new Date(now.getFullYear(), now.getMonth(), 1); + return { from: d.toISOString(), to }; + } + case "7d": { + const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0, 0); + return { from: d.toISOString(), to }; + } + case "30d": { + const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30, 0, 0, 0, 0); + return { from: d.toISOString(), to }; + } + case "ytd": { + const d = new Date(now.getFullYear(), 0, 1); + return { from: d.toISOString(), to }; + } + case "all": + case "custom": + return { from: "", to: "" }; + } +} + +// floor a Date to the nearest minute so the query key is stable across +// 30s refetch ticks (prevents new cache entries on every poll cycle) +function floorToMinute(d: Date): string { + const floored = new Date(d); + floored.setSeconds(0, 0); + return floored.toISOString(); +} + +export interface UseDateRangeResult { + preset: DatePreset; + setPreset: (p: DatePreset) => void; + customFrom: string; + setCustomFrom: (v: string) => void; + customTo: string; + setCustomTo: (v: string) => void; + /** resolved iso strings ready to pass to api calls; empty string means unbounded */ + from: string; + to: string; + /** false when preset=custom but both dates are not yet selected */ + customReady: boolean; +} + +export function useDateRange(): UseDateRangeResult { + const [preset, setPreset] = useState("mtd"); + const [customFrom, setCustomFrom] = useState(""); + const [customTo, setCustomTo] = useState(""); + + // tick at the next calendar minute boundary, then every 60s, so sliding presets + // (7d, 30d) advance their upper bound in sync with wall clock minutes rather than + // drifting by the mount offset. + const intervalRef = useRef | null>(null); + const [minuteTick, setMinuteTick] = useState(() => floorToMinute(new Date())); + useEffect(() => { + const now = new Date(); + const msToNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds(); + const timeout = setTimeout(() => { + setMinuteTick(floorToMinute(new Date())); + intervalRef.current = setInterval( + () => setMinuteTick(floorToMinute(new Date())), + 60_000, + ); + }, msToNextMinute); + return () => { + clearTimeout(timeout); + if (intervalRef.current != null) clearInterval(intervalRef.current); + }; + }, []); + + const { from, to } = useMemo(() => { + if (preset !== "custom") return computeRange(preset); + // treat custom date strings as local-date boundaries so the full day is included + // regardless of the user's timezone. "from" starts at local midnight, "to" at 23:59:59.999. + const fromDate = customFrom ? new Date(customFrom + "T00:00:00") : null; + const toDate = customTo ? new Date(customTo + "T23:59:59.999") : null; + return { + from: fromDate ? fromDate.toISOString() : "", + to: toDate ? toDate.toISOString() : "", + }; + // minuteTick drives re-evaluation of sliding presets once per minute. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [preset, customFrom, customTo, minuteTick]); + + const customReady = preset !== "custom" || (!!customFrom && !!customTo); + + return { + preset, + setPreset, + customFrom, + setCustomFrom, + customTo, + setCustomTo, + from, + to, + customReady, + }; +} diff --git a/ui/src/lib/company-routes.ts b/ui/src/lib/company-routes.ts index 512a4e61..9dd89cd4 100644 --- a/ui/src/lib/company-routes.ts +++ b/ui/src/lib/company-routes.ts @@ -10,6 +10,7 @@ const BOARD_ROUTE_ROOTS = new Set([ "goals", "approvals", "costs", + "usage", "activity", "inbox", "design-guide", 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 b7b91b5e..3eb8603b 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -52,6 +52,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, @@ -80,6 +83,22 @@ export const queryKeys = { activity: (companyId: string) => ["activity", companyId] as const, costs: (companyId: string, from?: string, to?: string) => ["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) => + ["usage-quota-windows", companyId] as const, heartbeats: (companyId: string, agentId?: string) => ["heartbeats", companyId, agentId] as const, runDetail: (runId: string) => ["heartbeat-run", runId] as const, diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index b9c3a020..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)); @@ -48,6 +49,98 @@ export function formatTokens(n: number): string { return String(n); } +/** Map a raw provider slug to a display-friendly name. */ +export function providerDisplayName(provider: string): string { + const map: Record = { + anthropic: "Anthropic", + openai: "OpenAI", + openrouter: "OpenRouter", + chatgpt: "ChatGPT", + google: "Google", + cursor: "Cursor", + jetbrains: "JetBrains AI", + }; + 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 83ffcee7..c891d6c3 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -3,6 +3,7 @@ import { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/r import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents"; import { companySkillsApi } from "../api/companySkills"; +import { budgetsApi } from "../api/budgets"; import { heartbeatsApi } from "../api/heartbeats"; import { ApiError } from "../api/client"; import { ChartCard, RunActivityChart, PriorityChart, IssueStatusChart, SuccessRateChart } from "../components/ActivityCharts"; @@ -25,8 +26,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"; @@ -60,7 +62,16 @@ 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 AgentRuntimeState, type AgentSkillSnapshot, type HeartbeatRun, type HeartbeatRunEvent, type LiveEvent } from "@paperclipai/shared"; +import { + isUuidLike, + type Agent, + type AgentSkillSnapshot, + 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"; @@ -183,11 +194,12 @@ function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBeh container.scrollTo({ top: container.scrollHeight, behavior }); } -type AgentDetailView = "dashboard" | "configuration" | "skills" | "runs"; +type AgentDetailView = "dashboard" | "configuration" | "skills" | "runs" | "budget"; function parseAgentDetailView(value: string | null): AgentDetailView { if (value === "configure" || value === "configuration") return "configuration"; if (value === "skills") return "skills"; + if (value === "budget") return "budget"; if (value === "runs") return value; return "dashboard"; } @@ -213,8 +225,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, @@ -306,11 +317,50 @@ export function AgentDetail() { enabled: !!resolvedCompanyId && needsDashboardData, }); + 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], @@ -331,7 +381,9 @@ export function AgentDetail() { ? "skills" : activeView === "runs" ? "runs" - : "dashboard"; + : activeView === "budget" + ? "budget" + : "dashboard"; if (routeAgentRef !== canonicalAgentRef || urlTab !== canonicalTab) { navigate(`/agents/${canonicalAgentRef}/${canonicalTab}`, { replace: true }); return; @@ -374,6 +426,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: () => { @@ -432,6 +502,8 @@ export function AgentDetail() { crumbs.push({ label: "Skills" }); } else if (activeView === "runs") { crumbs.push({ label: "Runs" }); + } else if (activeView === "budget") { + crumbs.push({ label: "Budget" }); } else { crumbs.push({ label: "Dashboard" }); } @@ -589,6 +661,7 @@ export function AgentDetail() { { value: "configuration", label: "Configuration" }, { value: "skills", label: "Skills" }, { value: "runs", label: "Runs" }, + { value: "budget", label: "Budget" }, ]} value={activeView} onValueChange={(value) => navigate(`/agents/${canonicalAgentRef}/${value}`)} @@ -701,6 +774,17 @@ export function AgentDetail() { adapterType={agent.adapterType} /> )} + + {activeView === "budget" && resolvedCompanyId ? ( +
+ budgetMutation.mutate(amount)} + variant="plain" + /> +
+ ) : null}
); } @@ -873,8 +957,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()); @@ -916,16 +1000,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 && ( <> +
+ )} + {(logoUploadMutation.isError || logoUploadError) && ( + + {logoUploadError ?? + (logoUploadMutation.error instanceof Error + ? logoUploadMutation.error.message + : "Logo upload failed")} + + )} + {clearLogoMutation.isError && ( + + {clearLogoMutation.error.message} + + )} + {logoUploadMutation.isPending && ( + Uploading logo... + )} +
+ {generalMutation.error instanceof Error - ? generalMutation.error.message - : "Failed to save"} + ? generalMutation.error.message + : "Failed to save"} )}
diff --git a/ui/src/pages/Costs.tsx b/ui/src/pages/Costs.tsx index 6b977928..281ae0e4 100644 --- a/ui/src/pages/Costs.tsx +++ b/ui/src/pages/Costs.tsx @@ -1,244 +1,1102 @@ -import { useEffect, useMemo, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useEffect, useMemo, useRef, useState, type ComponentType } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { + BudgetPolicySummary, + CostByAgentModel, + CostByBiller, + CostByProviderModel, + CostWindowSpendRow, + FinanceEvent, + QuotaWindow, +} from "@paperclipai/shared"; +import { ArrowDownLeft, ArrowUpRight, ChevronDown, ChevronRight, Coins, DollarSign, ReceiptText } from "lucide-react"; +import { budgetsApi } from "../api/budgets"; import { costsApi } from "../api/costs"; -import { useCompany } from "../context/CompanyContext"; -import { useBreadcrumbs } from "../context/BreadcrumbContext"; -import { queryKeys } from "../lib/queryKeys"; +import { BillerSpendCard } from "../components/BillerSpendCard"; +import { BudgetIncidentCard } from "../components/BudgetIncidentCard"; +import { BudgetPolicyCard } from "../components/BudgetPolicyCard"; import { EmptyState } from "../components/EmptyState"; -import { PageSkeleton } from "../components/PageSkeleton"; -import { formatCents, formatTokens } from "../lib/utils"; +import { FinanceBillerCard } from "../components/FinanceBillerCard"; +import { FinanceKindCard } from "../components/FinanceKindCard"; +import { FinanceTimelineCard } from "../components/FinanceTimelineCard"; import { Identity } from "../components/Identity"; +import { PageSkeleton } from "../components/PageSkeleton"; +import { PageTabBar } from "../components/PageTabBar"; +import { ProviderQuotaCard } from "../components/ProviderQuotaCard"; import { StatusBadge } from "../components/StatusBadge"; -import { Card, CardContent } from "@/components/ui/card"; +import { useBreadcrumbs } from "../context/BreadcrumbContext"; +import { useCompany } from "../context/CompanyContext"; +import { useDateRange, PRESET_KEYS, PRESET_LABELS } from "../hooks/useDateRange"; +import { queryKeys } from "../lib/queryKeys"; +import { billingTypeDisplayName, cn, formatCents, formatTokens, providerDisplayName } from "../lib/utils"; import { Button } from "@/components/ui/button"; -import { DollarSign } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -type DatePreset = "mtd" | "7d" | "30d" | "ytd" | "all" | "custom"; +const NO_COMPANY = "__none__"; -const PRESET_LABELS: Record = { - mtd: "Month to Date", - "7d": "Last 7 Days", - "30d": "Last 30 Days", - ytd: "Year to Date", - all: "All Time", - custom: "Custom", -}; - -function computeRange(preset: DatePreset): { from: string; to: string } { +function currentWeekRange(): { from: string; to: string } { const now = new Date(); - const to = now.toISOString(); - switch (preset) { - case "mtd": { - const d = new Date(now.getFullYear(), now.getMonth(), 1); - return { from: d.toISOString(), to }; - } - case "7d": { - const d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - return { from: d.toISOString(), to }; - } - case "30d": { - const d = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - return { from: d.toISOString(), to }; - } - case "ytd": { - const d = new Date(now.getFullYear(), 0, 1); - return { from: d.toISOString(), to }; - } - case "all": - return { from: "", to: "" }; - case "custom": - return { from: "", to: "" }; - } + const day = now.getDay(); + const diffToMon = day === 0 ? -6 : 1 - day; + const mon = new Date(now.getFullYear(), now.getMonth(), now.getDate() + diffToMon, 0, 0, 0, 0); + const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6, 23, 59, 59, 999); + return { from: mon.toISOString(), to: sun.toISOString() }; +} + +function ProviderTabLabel({ provider, rows }: { provider: string; rows: CostByProviderModel[] }) { + const totalTokens = rows.reduce((sum, row) => sum + row.inputTokens + row.cachedInputTokens + row.outputTokens, 0); + const totalCost = rows.reduce((sum, row) => sum + row.costCents, 0); + return ( + + {providerDisplayName(provider)} + {formatTokens(totalTokens)} + {formatCents(totalCost)} + + ); +} + +function BillerTabLabel({ biller, rows }: { biller: string; rows: CostByBiller[] }) { + const totalTokens = rows.reduce((sum, row) => sum + row.inputTokens + row.cachedInputTokens + row.outputTokens, 0); + const totalCost = rows.reduce((sum, row) => sum + row.costCents, 0); + return ( + + {providerDisplayName(biller)} + {formatTokens(totalTokens)} + {formatCents(totalCost)} + + ); +} + +function MetricTile({ + label, + value, + subtitle, + icon: Icon, +}: { + label: string; + value: string; + subtitle: string; + icon: ComponentType<{ className?: string }>; +}) { + return ( +
+
+
+
{label}
+
{value}
+
{subtitle}
+
+
+ +
+
+
+ ); +} + +function FinanceSummaryCard({ + debitCents, + creditCents, + netCents, + estimatedDebitCents, + eventCount, +}: { + debitCents: number; + creditCents: number; + netCents: number; + estimatedDebitCents: number; + eventCount: number; +}) { + return ( + + + Finance ledger + + Account-level charges that do not map to a single inference request. + + + + + + + + + + ); } export function Costs() { const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); + const queryClient = useQueryClient(); - const [preset, setPreset] = useState("mtd"); - const [customFrom, setCustomFrom] = useState(""); - const [customTo, setCustomTo] = useState(""); + const [mainTab, setMainTab] = useState<"overview" | "budgets" | "providers" | "billers" | "finance">("overview"); + const [activeProvider, setActiveProvider] = useState("all"); + const [activeBiller, setActiveBiller] = useState("all"); + + const { + preset, + setPreset, + customFrom, + setCustomFrom, + customTo, + setCustomTo, + from, + to, + customReady, + } = useDateRange(); useEffect(() => { setBreadcrumbs([{ label: "Costs" }]); }, [setBreadcrumbs]); - const { from, to } = useMemo(() => { - if (preset === "custom") { - return { - from: customFrom ? new Date(customFrom).toISOString() : "", - to: customTo ? new Date(customTo + "T23:59:59.999Z").toISOString() : "", - }; - } - return computeRange(preset); - }, [preset, customFrom, customTo]); + const [today, setToday] = useState(() => new Date().toDateString()); + const todayTimerRef = useRef | null>(null); + useEffect(() => { + const schedule = () => { + const now = new Date(); + const ms = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).getTime() - now.getTime(); + todayTimerRef.current = setTimeout(() => { + setToday(new Date().toDateString()); + schedule(); + }, ms); + }; + schedule(); + return () => { + if (todayTimerRef.current != null) clearTimeout(todayTimerRef.current); + }; + }, []); - const { data, isLoading, error } = useQuery({ - queryKey: queryKeys.costs(selectedCompanyId!, from || undefined, to || undefined), - queryFn: async () => { - const [summary, byAgent, byProject] = await Promise.all([ - costsApi.summary(selectedCompanyId!, from || undefined, to || undefined), - costsApi.byAgent(selectedCompanyId!, from || undefined, to || undefined), - costsApi.byProject(selectedCompanyId!, from || undefined, to || undefined), - ]); - return { summary, byAgent, byProject }; - }, - enabled: !!selectedCompanyId, + const weekRange = useMemo(() => currentWeekRange(), [today]); + const companyId = selectedCompanyId ?? NO_COMPANY; + + const { data: budgetData, isLoading: budgetLoading, error: budgetError } = useQuery({ + queryKey: queryKeys.budgets.overview(companyId), + queryFn: () => budgetsApi.overview(companyId), + enabled: !!selectedCompanyId && customReady, + refetchInterval: 30_000, + staleTime: 5_000, }); + const invalidateBudgetViews = () => { + if (!selectedCompanyId) return; + queryClient.invalidateQueries({ queryKey: queryKeys.budgets.overview(selectedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(selectedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.projects.list(selectedCompanyId) }); + }; + + const policyMutation = useMutation({ + mutationFn: (input: { + scopeType: BudgetPolicySummary["scopeType"]; + scopeId: string; + amount: number; + windowKind: BudgetPolicySummary["windowKind"]; + }) => + budgetsApi.upsertPolicy(companyId, { + scopeType: input.scopeType, + scopeId: input.scopeId, + amount: input.amount, + windowKind: input.windowKind, + }), + onSuccess: invalidateBudgetViews, + }); + + const incidentMutation = useMutation({ + mutationFn: (input: { incidentId: string; action: "keep_paused" | "raise_budget_and_resume"; amount?: number }) => + budgetsApi.resolveIncident(companyId, input.incidentId, input), + onSuccess: invalidateBudgetViews, + }); + + const { data: spendData, isLoading: spendLoading, error: spendError } = useQuery({ + queryKey: queryKeys.costs(companyId, from || undefined, to || undefined), + queryFn: async () => { + const [summary, byAgent, byProject, byAgentModel] = await Promise.all([ + costsApi.summary(companyId, from || undefined, to || undefined), + costsApi.byAgent(companyId, from || undefined, to || undefined), + costsApi.byProject(companyId, from || undefined, to || undefined), + costsApi.byAgentModel(companyId, from || undefined, to || undefined), + ]); + return { summary, byAgent, byProject, byAgentModel }; + }, + enabled: !!selectedCompanyId && customReady, + }); + + const { data: financeData, isLoading: financeLoading, error: financeError } = useQuery({ + queryKey: [ + queryKeys.financeSummary(companyId, from || undefined, to || undefined), + queryKeys.financeByBiller(companyId, from || undefined, to || undefined), + queryKeys.financeByKind(companyId, from || undefined, to || undefined), + queryKeys.financeEvents(companyId, from || undefined, to || undefined, 18), + ], + queryFn: async () => { + const [summary, byBiller, byKind, events] = await Promise.all([ + costsApi.financeSummary(companyId, from || undefined, to || undefined), + costsApi.financeByBiller(companyId, from || undefined, to || undefined), + costsApi.financeByKind(companyId, from || undefined, to || undefined), + costsApi.financeEvents(companyId, from || undefined, to || undefined, 18), + ]); + return { summary, byBiller, byKind, events }; + }, + enabled: !!selectedCompanyId && customReady, + }); + + const [expandedAgents, setExpandedAgents] = useState>(new Set()); + useEffect(() => { + setExpandedAgents(new Set()); + }, [companyId, from, to]); + + function toggleAgent(agentId: string) { + setExpandedAgents((prev) => { + const next = new Set(prev); + if (next.has(agentId)) next.delete(agentId); + else next.add(agentId); + return next; + }); + } + + const agentModelRows = useMemo(() => { + const map = new Map(); + for (const row of spendData?.byAgentModel ?? []) { + const rows = map.get(row.agentId) ?? []; + rows.push(row); + map.set(row.agentId, rows); + } + for (const [agentId, rows] of map) { + map.set(agentId, rows.slice().sort((a, b) => b.costCents - a.costCents)); + } + return map; + }, [spendData?.byAgentModel]); + + const { data: providerData } = useQuery({ + queryKey: queryKeys.usageByProvider(companyId, from || undefined, to || undefined), + queryFn: () => costsApi.byProvider(companyId, from || undefined, to || undefined), + enabled: !!selectedCompanyId && customReady && (mainTab === "providers" || mainTab === "billers"), + refetchInterval: 30_000, + staleTime: 10_000, + }); + + const { data: billerData } = useQuery({ + queryKey: queryKeys.usageByBiller(companyId, from || undefined, to || undefined), + queryFn: () => costsApi.byBiller(companyId, from || undefined, to || undefined), + enabled: !!selectedCompanyId && customReady && mainTab === "billers", + refetchInterval: 30_000, + staleTime: 10_000, + }); + + const { data: weekData } = useQuery({ + queryKey: queryKeys.usageByProvider(companyId, weekRange.from, weekRange.to), + queryFn: () => costsApi.byProvider(companyId, weekRange.from, weekRange.to), + enabled: !!selectedCompanyId && (mainTab === "providers" || mainTab === "billers"), + refetchInterval: 30_000, + staleTime: 10_000, + }); + + const { data: weekBillerData } = useQuery({ + queryKey: queryKeys.usageByBiller(companyId, weekRange.from, weekRange.to), + queryFn: () => costsApi.byBiller(companyId, weekRange.from, weekRange.to), + enabled: !!selectedCompanyId && mainTab === "billers", + refetchInterval: 30_000, + staleTime: 10_000, + }); + + const { data: windowData } = useQuery({ + queryKey: queryKeys.usageWindowSpend(companyId), + queryFn: () => costsApi.windowSpend(companyId), + enabled: !!selectedCompanyId && mainTab === "providers", + refetchInterval: 30_000, + staleTime: 10_000, + }); + + const { data: quotaData, isLoading: quotaLoading } = useQuery({ + queryKey: queryKeys.usageQuotaWindows(companyId), + queryFn: () => costsApi.quotaWindows(companyId), + enabled: !!selectedCompanyId && mainTab === "providers", + refetchInterval: 300_000, + staleTime: 60_000, + }); + + const byProvider = useMemo(() => { + const map = new Map(); + for (const row of providerData ?? []) { + const rows = map.get(row.provider) ?? []; + rows.push(row); + map.set(row.provider, rows); + } + return map; + }, [providerData]); + + const byBiller = useMemo(() => { + const map = new Map(); + for (const row of billerData ?? []) { + const rows = map.get(row.biller) ?? []; + rows.push(row); + map.set(row.biller, rows); + } + return map; + }, [billerData]); + + const weekSpendByProvider = useMemo(() => { + const map = new Map(); + for (const row of weekData ?? []) { + map.set(row.provider, (map.get(row.provider) ?? 0) + row.costCents); + } + return map; + }, [weekData]); + + const weekSpendByBiller = useMemo(() => { + const map = new Map(); + for (const row of weekBillerData ?? []) { + map.set(row.biller, (map.get(row.biller) ?? 0) + row.costCents); + } + return map; + }, [weekBillerData]); + + const windowSpendByProvider = useMemo(() => { + const map = new Map(); + for (const row of windowData ?? []) { + const rows = map.get(row.provider) ?? []; + rows.push(row); + map.set(row.provider, rows); + } + return map; + }, [windowData]); + + const quotaWindowsByProvider = useMemo(() => { + const map = new Map(); + for (const result of quotaData ?? []) { + if (result.ok && result.windows.length > 0) { + map.set(result.provider, result.windows); + } + } + return map; + }, [quotaData]); + + const quotaErrorsByProvider = useMemo(() => { + const map = new Map(); + for (const result of quotaData ?? []) { + if (!result.ok && result.error) map.set(result.provider, result.error); + } + return map; + }, [quotaData]); + + const quotaSourcesByProvider = useMemo(() => { + const map = new Map(); + for (const result of quotaData ?? []) { + if (typeof result.source === "string" && result.source.length > 0) { + map.set(result.provider, result.source); + } + } + return map; + }, [quotaData]); + + const deficitNotchByProvider = useMemo(() => { + const map = new Map(); + if (preset !== "mtd") return map; + const budget = spendData?.summary.budgetCents ?? 0; + if (budget <= 0) return map; + const totalSpend = spendData?.summary.spendCents ?? 0; + const now = new Date(); + const daysElapsed = now.getDate(); + const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate(); + for (const [providerKey, rows] of byProvider) { + const providerCostCents = rows.reduce((sum, row) => sum + row.costCents, 0); + const providerShare = totalSpend > 0 ? providerCostCents / totalSpend : 0; + const providerBudget = budget * providerShare; + if (providerBudget <= 0) { + map.set(providerKey, false); + continue; + } + const burnRate = providerCostCents / Math.max(daysElapsed, 1); + map.set(providerKey, providerCostCents + burnRate * (daysInMonth - daysElapsed) > providerBudget); + } + return map; + }, [preset, spendData, byProvider]); + + const providers = useMemo(() => Array.from(byProvider.keys()), [byProvider]); + const billers = useMemo(() => Array.from(byBiller.keys()), [byBiller]); + + const effectiveProvider = + activeProvider === "all" || providers.includes(activeProvider) ? activeProvider : "all"; + useEffect(() => { + if (effectiveProvider !== activeProvider) setActiveProvider("all"); + }, [effectiveProvider, activeProvider]); + + const effectiveBiller = + activeBiller === "all" || billers.includes(activeBiller) ? activeBiller : "all"; + useEffect(() => { + if (effectiveBiller !== activeBiller) setActiveBiller("all"); + }, [effectiveBiller, activeBiller]); + + const providerTabItems = useMemo(() => { + const providerKeys = Array.from(byProvider.keys()); + const allTokens = providerKeys.reduce( + (sum, provider) => sum + (byProvider.get(provider)?.reduce((acc, row) => acc + row.inputTokens + row.cachedInputTokens + row.outputTokens, 0) ?? 0), + 0, + ); + const allCents = providerKeys.reduce( + (sum, provider) => sum + (byProvider.get(provider)?.reduce((acc, row) => acc + row.costCents, 0) ?? 0), + 0, + ); + return [ + { + value: "all", + label: ( + + All providers + {providerKeys.length > 0 ? ( + <> + {formatTokens(allTokens)} + {formatCents(allCents)} + + ) : null} + + ), + }, + ...providerKeys.map((provider) => ({ + value: provider, + label: , + })), + ]; + }, [byProvider]); + + const billerTabItems = useMemo(() => { + const billerKeys = Array.from(byBiller.keys()); + const allTokens = billerKeys.reduce( + (sum, biller) => sum + (byBiller.get(biller)?.reduce((acc, row) => acc + row.inputTokens + row.cachedInputTokens + row.outputTokens, 0) ?? 0), + 0, + ); + const allCents = billerKeys.reduce( + (sum, biller) => sum + (byBiller.get(biller)?.reduce((acc, row) => acc + row.costCents, 0) ?? 0), + 0, + ); + return [ + { + value: "all", + label: ( + + All billers + {billerKeys.length > 0 ? ( + <> + {formatTokens(allTokens)} + {formatCents(allCents)} + + ) : null} + + ), + }, + ...billerKeys.map((biller) => ({ + value: biller, + label: , + })), + ]; + }, [byBiller]); + + const inferenceTokenTotal = + (spendData?.byAgent ?? []).reduce( + (sum, row) => sum + row.inputTokens + row.cachedInputTokens + row.outputTokens, + 0, + ); + + const topFinanceEvents = (financeData?.events ?? []) as FinanceEvent[]; + const budgetPolicies = budgetData?.policies ?? []; + const activeBudgetIncidents = budgetData?.activeIncidents ?? []; + const budgetPoliciesByScope = useMemo(() => ({ + company: budgetPolicies.filter((policy) => policy.scopeType === "company"), + agent: budgetPolicies.filter((policy) => policy.scopeType === "agent"), + project: budgetPolicies.filter((policy) => policy.scopeType === "project"), + }), [budgetPolicies]); + if (!selectedCompanyId) { return ; } - if (isLoading) { - return ; - } - - const presetKeys: DatePreset[] = ["mtd", "7d", "30d", "ytd", "all", "custom"]; + const showCustomPrompt = preset === "custom" && !customReady; + const showOverviewLoading = (spendLoading || financeLoading) && customReady; + const overviewError = spendError ?? financeError; return (
- {/* Date range selector */} -
- {presetKeys.map((p) => ( - - ))} - {preset === "custom" && ( -
- setCustomFrom(e.target.value)} - className="h-8 rounded-md 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 rounded-md 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} + /> + +
- )}
- {error &&

{error.message}

} + setMainTab(value as typeof mainTab)}> + + Overview + Budgets + Providers + Billers + Finance + - {data && ( - <> - {/* Summary card */} - - -
-

{PRESET_LABELS[preset]}

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

- {data.summary.utilizationPercent}% utilized -

- )} -
-

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

- {data.summary.budgetCents > 0 && ( -
-
90 - ? "bg-red-400" - : data.summary.utilizationPercent > 70 - ? "bg-yellow-400" - : "bg-green-400" - }`} - style={{ width: `${Math.min(100, data.summary.utilizationPercent)}%` }} - /> + + {showCustomPrompt ? ( +

Select a start and end date to load data.

+ ) : showOverviewLoading ? ( + + ) : overviewError ? ( +

{(overviewError as Error).message}

+ ) : ( + <> + {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} - {/* By Agent / By Project */} -
- - -

By Agent

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

No cost events yet.

- ) : ( -
- {data.byAgent.map((row) => ( -
-
- + + + 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)}%` }} /> - {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} - - )} +
+ {spendData.summary.utilizationPercent}% of monthly budget consumed in this range.
- ))} -
- )} - - + ) : null} + + - - -

By Project

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

No project-attributed run costs yet.

- ) : ( -
- {data.byProject.map((row) => ( -
- - {row.projectName ?? row.projectId ?? "Unattributed"} - - {formatCents(row.costCents)} -
+ +
+ +
+ + + 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. + + + + + + + + + + + {activeBudgetIncidents.length > 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} + +
+ {(["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..4de40782 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"; @@ -24,7 +26,7 @@ import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slo /* ── Top-level tab types ── */ -type ProjectBaseTab = "overview" | "list" | "configuration"; +type ProjectBaseTab = "overview" | "list" | "configuration" | "budget"; type ProjectPluginTab = `plugin:${string}`; type ProjectTab = ProjectBaseTab | ProjectPluginTab; @@ -39,6 +41,7 @@ function resolveProjectTab(pathname: string, projectId: string): ProjectTab | nu const tab = segments[projectsIdx + 2]; if (tab === "overview") return "overview"; if (tab === "configuration") return "configuration"; + if (tab === "budget") return "budget"; if (tab === "issues") return "list"; return null; } @@ -296,6 +299,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" }, @@ -318,6 +329,10 @@ export function ProjectDetail() { navigate(`/projects/${canonicalProjectRef}/configuration`, { replace: true }); return; } + if (activeTab === "budget") { + navigate(`/projects/${canonicalProjectRef}/budget`, { replace: true }); + return; + } if (activeTab === "list") { if (filter) { navigate(`/projects/${canonicalProjectRef}/issues/${filter}`, { replace: true }); @@ -377,6 +392,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 ; } @@ -397,6 +459,8 @@ export function ProjectDetail() { } if (tab === "overview") { navigate(`/projects/${canonicalProjectRef}/overview`); + } else if (tab === "budget") { + navigate(`/projects/${canonicalProjectRef}/budget`); } else if (tab === "configuration") { navigate(`/projects/${canonicalProjectRef}/configuration`); } else { @@ -413,12 +477,20 @@ export function ProjectDetail() { onSelect={(color) => updateProject.mutate({ color })} />
- updateProject.mutate({ name })} - as="h2" - className="text-xl font-bold" - /> +
+ updateProject.mutate({ name })} + as="h2" + className="text-xl font-bold" + /> + {project.pauseReason === "budget" ? ( +
+ + Paused by budget hard stop +
+ ) : null} +
({ value: item.value, label: item.label, @@ -497,6 +570,17 @@ export function ProjectDetail() {
)} + {activeTab === "budget" && resolvedCompanyId ? ( +
+ budgetMutation.mutate(amount)} + /> +
+ ) : null} + {activePluginTab && (