From 0aaf85d800685c128cfe009ed003afc8a1166d32 Mon Sep 17 00:00:00 2001 From: John Hu Date: Mon, 27 Jul 2026 21:19:21 +0800 Subject: [PATCH 1/8] docs(adr): record retry re-resolution semantics (ADR-0014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A live incident showed task retry is a trap for configuration failures: it copies the frozen model resolution verbatim, so after fixing grants the retried run re-bills the whole pipeline from checkout and fails at the same step. ADR-0014 decides that retry becomes a re-submission of the pinned task — template version and params stay pinned, everything configuration-derived (bindings, models, manifest, budget, quota, repo refs) re-resolves — because re-resolution is deterministic when config is unchanged and auto-classification of resolution-shaped failures is impossible (the incident finalized as catch-all model_error). Also decides persisting submit-time agent overrides, deleting the subsumed assertResolutionCredentialed gate, and the UI confirmation. --- .../0014-retry-re-resolves-configuration.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/adr/0014-retry-re-resolves-configuration.md diff --git a/docs/adr/0014-retry-re-resolves-configuration.md b/docs/adr/0014-retry-re-resolves-configuration.md new file mode 100644 index 0000000..2ff2465 --- /dev/null +++ b/docs/adr/0014-retry-re-resolves-configuration.md @@ -0,0 +1,33 @@ +# ADR-0014: Retry Re-Resolves Agent Bindings Against Current Configuration + +- Status: accepted · Date: 2026-07-27 + +## Context + +Task retry (`POST /tasks/:id/retry`) copied every configuration-derived field of the previous run verbatim — `agentBindings`, `modelResolution`, `resourceManifest`, `budget` — under the comment "pinned — retries never re-resolve". That pin was designed for transient failures (provider hiccups, worker crashes): same inputs, same plan, reproducible. But it makes retry structurally unable to heal the other major failure class, configuration failures, and the cost asymmetry is brutal: a retried run re-executes the whole pipeline from checkout (clarify loop, plan approval, full implementation — all billed) and then dies at the exact step the stale resolution doomed it to. The motivating incident: a requirement-delivery run whose reviewer slot froze `gpt-5.1-codex`, a model id the deployment's ChatGPT-account Codex auth cannot serve; the operator fixed the project's grants, and the only working recovery was re-filling the submission form — the Retry button was a trap that would have re-billed everything and failed identically. + +Two facts settled the design space. First, resolution-shaped failures are not classifiable from `runs.error.code`: the incident finalized as the catch-all `model_error` (executors normalize most upstream rejections to it), and `executor_unavailable` never reaches a run's error at all (it is a submit-time code; the runtime analogue is a worker decline that leaves the run deferred) — so any "auto-re-resolve only when the failure looks configuration-shaped" scheme degenerates into message sniffing. Second, `resolveAgentBindings` is deterministic given the same project configuration: when nothing changed since submit, re-resolving reproduces the previous bindings exactly, so re-resolution strictly generalizes copying — behavior differs only when configuration changed, which is precisely when the operator wants it to. + +## Decision + +1. **Retry is a re-submission of the pinned task.** The template version and params snapshot stay pinned (a republished template must not change a task's plan or inputs mid-flight — the existing `params_snapshot` rationale in the domain model is untouched). Everything derived from *project configuration* is re-derived exactly as submit derives it: agent bindings and model resolution (`resolveAgentBindings`, including executor availability, capability, grant, registry-status, and credential checks), the resource manifest (`authorizeResources` — a revoked skill grant no longer rides along on a copied manifest), the budget (from the pinned compiled spec), quota headroom, and repo-ref ownership. Submit and retry share one resolution helper so the two paths cannot drift. +2. **Submit-time agent overrides persist on `tasks` and re-apply at retry.** The `agents` overrides a user picks on the submit form were consumed at resolution and stored nowhere, so a re-resolving retry could not distinguish "the user deliberately chose this executor" from "the template default resolved to it". A new `tasks.agent_overrides` column records the raw override object at submit; retry passes it back through resolution. Tasks created before the column exists retry with template defaults — a recorded consequence, not a bug. +3. **Freezing remains per-run, never per-task.** Each run still records the resolution it actually executed with — `runs.model_resolution` stays the execution and pricing record, frozen at run creation, because grants can change mid-run and usage attribution must be unambiguous. This ADR narrows the freeze's scope, not its existence: reproducibility of a run's *record* is intact; determinism of *retry-follows-previous-choice* is what changes. +4. **`assertResolutionCredentialed` is deleted, subsumed by the full gate.** ADR-0013 amendment 1 added it so a credential deleted between runs failed at the retry endpoint instead of mid-run; full re-resolution enforces the same `provider_credential_required` gate (and every stronger one) before the run row exists, so the narrow check has no remaining caller. +5. **Retry fails fast with submit's error codes, and ignores task-type disabling.** Current configuration that cannot satisfy a slot surfaces at the endpoint as the same 400s submit produces (`no_models_granted`, `model_unresolvable`, `provider_credential_required`, `executor_unavailable`, `executor_capability`, `quota_exhausted`, …) — never mid-run. `taskType.enabled` is deliberately not checked: disabling a task type blocks *new* submissions from the catalog, not the retry of a task that already exists. +6. **The UI states what retry means before it runs.** Retrying re-executes from checkout and re-asks earlier checkpoints (answered questions, approved plans); the Retry button gains a confirmation stating both that and that the run binds against today's configuration. Re-resolved bindings are visible where they always were — the run detail header chips and the Details card render each run's own resolution. + +## Alternatives considered + +- **Keep copying (status quo).** Preserves determinism nobody benefits from: with unchanged config, re-resolution yields the same bindings anyway; with changed config, the copy re-bills a full pipeline into a predetermined failure. +- **A second explicit action ("Retry with current configuration") beside plain Retry.** Clearest contract on paper, but two buttons where operators almost always want the second, and the first remains the trap this ADR exists to remove. +- **Auto-re-resolve only on resolution-shaped failures.** Not implementable honestly — `model_error` is a catch-all and `executor_unavailable` never lands in `runs.error.code`, so classification means sniffing free-text messages. +- **Reconstructing override intent by diffing `agentBindings` against template defaults.** Fragile: the `__default__` sentinel and demo-mode rebinding make "what the default would have been" depend on deployment env at submit time; persisting the user's actual input is exact and cheap. + +## Consequences + +- The incident's task becomes retryable: after fixing grants, retry binds the reviewer to a servable model and proceeds — no form re-fill, no doomed re-run. +- A retry after a config change can bind a different provider, executor, or model than run #1 — per ADR-0013's consequence, that is the intended meaning of "the project chose this"; each run's own record shows what it ran with. +- A retry can now be rejected for reasons the original submit passed (quota exhausted since, all grants revoked, executor no longer registered) — an earlier, cheaper failure than the mid-run one it replaces. `executor_unavailable` and its sibling codes join the localized error catalog for the first time on this path. +- Tasks predating `agent_overrides` retry with template defaults; a user who overrode a slot on such a task and needs the override back re-submits once, after which the override persists. +- Retry now writes inside a transaction (run insert + `tasks.latestRunId` together) and its audit row records the source run — parity with submit's atomicity and traceability. From ee4c9eea49c330f814c0855fbed20399a190fdae Mon Sep 17 00:00:00 2001 From: John Hu Date: Mon, 27 Jul 2026 21:20:15 +0800 Subject: [PATCH 2/8] feat(db): persist submit-time agent overrides on tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agents overrides a user picks on the submit form were consumed at resolution and stored nowhere, so nothing downstream could distinguish a deliberate slot choice from a template default that happened to resolve the same way. A re-resolving retry (ADR-0014) needs the raw override object back, so it now lands on tasks.agent_overrides (migration 0008, default {}). Tasks created before the column exists retry with template defaults — recorded as an ADR consequence. --- apps/api/src/routes/execution.ts | 1 + docs/design/01-domain-model.md | 1 + packages/db/drizzle/0008_agent-overrides.sql | 1 + packages/db/drizzle/meta/0008_snapshot.json | 3505 ++++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/runs.ts | 6 + 6 files changed, 3521 insertions(+) create mode 100644 packages/db/drizzle/0008_agent-overrides.sql create mode 100644 packages/db/drizzle/meta/0008_snapshot.json diff --git a/apps/api/src/routes/execution.ts b/apps/api/src/routes/execution.ts index 47e6d84..7ccd8c5 100644 --- a/apps/api/src/routes/execution.ts +++ b/apps/api/src/routes/execution.ts @@ -338,6 +338,7 @@ export const executionRoutes = new Hono() taskTypeId: taskType.id, title: input.title, params: parsed.data, + agentOverrides: input.agents ?? {}, createdBy: user.id, }) .returning(); diff --git a/docs/design/01-domain-model.md b/docs/design/01-domain-model.md index 711f694..2202780 100644 --- a/docs/design/01-domain-model.md +++ b/docs/design/01-domain-model.md @@ -157,6 +157,7 @@ models (id pk, org_id fk, provider text, -- 'anthropic' | . ```sql tasks (id pk, org_id fk, project_id fk, task_type_id fk, title, params jsonb, -- validated against template inputs + agent_overrides jsonb, -- raw submit-time slot overrides (ADR-0014) latest_run_id fk null, created_by fk, created_at) runs (id pk, task_id fk, diff --git a/packages/db/drizzle/0008_agent-overrides.sql b/packages/db/drizzle/0008_agent-overrides.sql new file mode 100644 index 0000000..c0d36f4 --- /dev/null +++ b/packages/db/drizzle/0008_agent-overrides.sql @@ -0,0 +1 @@ +ALTER TABLE "tasks" ADD COLUMN "agent_overrides" jsonb DEFAULT '{}'::jsonb NOT NULL; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0008_snapshot.json b/packages/db/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..d9bef8f --- /dev/null +++ b/packages/db/drizzle/meta/0008_snapshot.json @@ -0,0 +1,3505 @@ +{ + "id": "656620e2-d0fa-4b4d-b87b-51bc6f20c324", + "prevId": "63dc5e4a-330c-4282-9637-6ee8ca419b01", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_org_id_orgs_id_fk": { + "name": "api_keys_org_id_orgs_id_fk", + "tableFrom": "api_keys", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "api_keys_project_id_projects_id_fk": { + "name": "api_keys_project_id_projects_id_fk", + "tableFrom": "api_keys", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "api_keys_created_by_users_id_fk": { + "name": "api_keys_created_by_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_api_key_id": { + "name": "actor_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_logs_org_time_idx": { + "name": "audit_logs_org_time_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_org_id_orgs_id_fk": { + "name": "audit_logs_org_id_orgs_id_fk", + "tableFrom": "audit_logs", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_logs_project_id_projects_id_fk": { + "name": "audit_logs_project_id_projects_id_fk", + "tableFrom": "audit_logs", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_logs_actor_user_id_users_id_fk": { + "name": "audit_logs_actor_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": ["actor_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_logs_actor_api_key_id_api_keys_id_fk": { + "name": "audit_logs_actor_api_key_id_api_keys_id_fk", + "tableFrom": "audit_logs", + "tableTo": "api_keys", + "columnsFrom": ["actor_api_key_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "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 + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "org_role": { + "name": "org_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org_member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_org_id_orgs_id_fk": { + "name": "users_org_id_orgs_id_fk", + "tableFrom": "users", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "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": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org_member'" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_user_id": { + "name": "accepted_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitations_org_idx": { + "name": "invitations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitations_token_idx": { + "name": "invitations_token_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitations_org_id_orgs_id_fk": { + "name": "invitations_org_id_orgs_id_fk", + "tableFrom": "invitations", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invitations_created_by_users_id_fk": { + "name": "invitations_created_by_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invitations_accepted_user_id_users_id_fk": { + "name": "invitations_accepted_user_id_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": ["accepted_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitations_token_hash_unique": { + "name": "invitations_token_hash_unique", + "nullsNotDistinct": false, + "columns": ["token_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orgs": { + "name": "orgs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "orgs_slug_unique": { + "name": "orgs_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_members": { + "name": "project_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_members_uq": { + "name": "project_members_uq", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_members_project_id_projects_id_fk": { + "name": "project_members_project_id_projects_id_fk", + "tableFrom": "project_members", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_members_user_id_users_id_fk": { + "name": "project_members_user_id_users_id_fk", + "tableFrom": "project_members", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_quotas": { + "name": "project_quotas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + }, + "token_limit": { + "name": "token_limit", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cost_limit_usd": { + "name": "cost_limit_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "hard_stop": { + "name": "hard_stop", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "current_period_start": { + "name": "current_period_start", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_quotas_project_id_projects_id_fk": { + "name": "project_quotas_project_id_projects_id_fk", + "tableFrom": "project_quotas", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_quotas_project_id_unique": { + "name": "project_quotas_project_id_unique", + "nullsNotDistinct": false, + "columns": ["project_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_resource_grants": { + "name": "project_resource_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_override": { + "name": "config_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_grants_uq": { + "name": "project_grants_uq", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_resource_grants_project_id_projects_id_fk": { + "name": "project_resource_grants_project_id_projects_id_fk", + "tableFrom": "project_resource_grants", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_resource_grants_granted_by_users_id_fk": { + "name": "project_resource_grants_granted_by_users_id_fk", + "tableFrom": "project_resource_grants", + "tableTo": "users", + "columnsFrom": ["granted_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_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 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "projects_org_slug_uq": { + "name": "projects_org_slug_uq", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_org_id_orgs_id_fk": { + "name": "projects_org_id_orgs_id_fk", + "tableFrom": "projects", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_created_by_users_id_fk": { + "name": "projects_created_by_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_credentials": { + "name": "provider_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_ref": { + "name": "secret_ref", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_credentials_uq": { + "name": "provider_credentials_uq", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_credentials_project_id_projects_id_fk": { + "name": "provider_credentials_project_id_projects_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "provider_credentials_secret_ref_secrets_id_fk": { + "name": "provider_credentials_secret_ref_secrets_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "secrets", + "columnsFrom": ["secret_ref"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repo_connections": { + "name": "repo_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "credential_secret_ref": { + "name": "credential_secret_ref", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "repo_connections_project_id_projects_id_fk": { + "name": "repo_connections_project_id_projects_id_fk", + "tableFrom": "repo_connections", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "repo_connections_credential_secret_ref_secrets_id_fk": { + "name": "repo_connections_credential_secret_ref_secrets_id_fk", + "tableFrom": "repo_connections", + "tableTo": "secrets", + "columnsFrom": ["credential_secret_ref"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.executor_registrations": { + "name": "executor_registrations", + "schema": "", + "columns": { + "executor_id": { + "name": "executor_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fabri": { + "name": "fabri", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_i18n": { + "name": "name_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "persona_i18n": { + "name": "persona_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model_role_policy": { + "name": "default_model_role_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fabri_org_id_orgs_id_fk": { + "name": "fabri_org_id_orgs_id_fk", + "tableFrom": "fabri", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fabri_slug_unique": { + "name": "fabri_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_i18n": { + "name": "name_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "auth_secret_ref": { + "name": "auth_secret_ref", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_servers_org_id_orgs_id_fk": { + "name": "mcp_servers_org_id_orgs_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_servers_auth_secret_ref_secrets_id_fk": { + "name": "mcp_servers_auth_secret_ref_secrets_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "secrets", + "columnsFrom": ["auth_secret_ref"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mcp_servers_slug_unique": { + "name": "mcp_servers_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models": { + "name": "models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_model_id": { + "name": "provider_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "input_cost_per_mtok": { + "name": "input_cost_per_mtok", + "type": "numeric(12, 4)", + "primaryKey": false, + "notNull": false + }, + "output_cost_per_mtok": { + "name": "output_cost_per_mtok", + "type": "numeric(12, 4)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "models_org_id_orgs_id_fk": { + "name": "models_org_id_orgs_id_fk", + "tableFrom": "models", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "models_provider_model_id_unique": { + "name": "models_provider_model_id_unique", + "nullsNotDistinct": false, + "columns": ["provider_model_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orchestration_templates": { + "name": "orchestration_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name_i18n": { + "name": "name_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "latest_published_version_id": { + "name": "latest_published_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "orchestration_templates_org_id_orgs_id_fk": { + "name": "orchestration_templates_org_id_orgs_id_fk", + "tableFrom": "orchestration_templates", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "orchestration_templates_scenario_id_scenarios_id_fk": { + "name": "orchestration_templates_scenario_id_scenarios_id_fk", + "tableFrom": "orchestration_templates", + "tableTo": "scenarios", + "columnsFrom": ["scenario_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "orchestration_templates_latest_published_version_id_template_versions_id_fk": { + "name": "orchestration_templates_latest_published_version_id_template_versions_id_fk", + "tableFrom": "orchestration_templates", + "tableTo": "template_versions", + "columnsFrom": ["latest_published_version_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "orchestration_templates_created_by_users_id_fk": { + "name": "orchestration_templates_created_by_users_id_fk", + "tableFrom": "orchestration_templates", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "orchestration_templates_slug_unique": { + "name": "orchestration_templates_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenarios": { + "name": "scenarios", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_i18n": { + "name": "name_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "description_i18n": { + "name": "description_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scenarios_org_id_orgs_id_fk": { + "name": "scenarios_org_id_orgs_id_fk", + "tableFrom": "scenarios", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scenarios_slug_unique": { + "name": "scenarios_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_versions": { + "name": "skill_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_ref": { + "name": "content_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_versions_uq": { + "name": "skill_versions_uq", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_versions_skill_id_skills_id_fk": { + "name": "skill_versions_skill_id_skills_id_fk", + "tableFrom": "skill_versions", + "tableTo": "skills", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_i18n": { + "name": "name_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "description_i18n": { + "name": "description_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_version_id": { + "name": "latest_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "skills_org_id_orgs_id_fk": { + "name": "skills_org_id_orgs_id_fk", + "tableFrom": "skills", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "skills_latest_version_id_skill_versions_id_fk": { + "name": "skills_latest_version_id_skill_versions_id_fk", + "tableFrom": "skills", + "tableTo": "skill_versions", + "columnsFrom": ["latest_version_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_slug_unique": { + "name": "skills_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_types": { + "name": "task_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_i18n": { + "name": "name_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "description_i18n": { + "name": "description_i18n", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "default_faber_id": { + "name": "default_faber_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_types_uq": { + "name": "task_types_uq", + "columns": [ + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_types_scenario_id_scenarios_id_fk": { + "name": "task_types_scenario_id_scenarios_id_fk", + "tableFrom": "task_types", + "tableTo": "scenarios", + "columnsFrom": ["scenario_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "task_types_template_id_orchestration_templates_id_fk": { + "name": "task_types_template_id_orchestration_templates_id_fk", + "tableFrom": "task_types", + "tableTo": "orchestration_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "task_types_default_faber_id_fabri_id_fk": { + "name": "task_types_default_faber_id_fabri_id_fk", + "tableFrom": "task_types", + "tableTo": "fabri", + "columnsFrom": ["default_faber_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_versions": { + "name": "template_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "source_yaml": { + "name": "source_yaml", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "compiled": { + "name": "compiled", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "template_versions_uq": { + "name": "template_versions_uq", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_versions_template_id_orchestration_templates_id_fk": { + "name": "template_versions_template_id_orchestration_templates_id_fk", + "tableFrom": "template_versions", + "tableTo": "orchestration_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_versions_created_by_users_id_fk": { + "name": "template_versions_created_by_users_id_fk", + "tableFrom": "template_versions", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifacts": { + "name": "artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "iteration": { + "name": "iteration", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime": { + "name": "mime", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "storage_ref": { + "name": "storage_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inline": { + "name": "inline", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "artifacts_run_id_runs_id_fk": { + "name": "artifacts_run_id_runs_id_fk", + "tableFrom": "artifacts", + "tableTo": "runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "artifacts_step_id_run_steps_id_fk": { + "name": "artifacts_step_id_run_steps_id_fk", + "tableFrom": "artifacts", + "tableTo": "run_steps", + "columnsFrom": ["step_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkpoints": { + "name": "checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'approval'" + }, + "iteration": { + "name": "iteration", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decided_by": { + "name": "decided_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "checkpoints_run_ckpt_iter_uq": { + "name": "checkpoints_run_ckpt_iter_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "iteration", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "checkpoints_run_id_runs_id_fk": { + "name": "checkpoints_run_id_runs_id_fk", + "tableFrom": "checkpoints", + "tableTo": "runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "checkpoints_step_id_run_steps_id_fk": { + "name": "checkpoints_step_id_run_steps_id_fk", + "tableFrom": "checkpoints", + "tableTo": "run_steps", + "columnsFrom": ["step_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "checkpoints_decided_by_users_id_fk": { + "name": "checkpoints_decided_by_users_id_fk", + "tableFrom": "checkpoints", + "tableTo": "users", + "columnsFrom": ["decided_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.run_comments": { + "name": "run_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "indexes": { + "run_comments_run_idx": { + "name": "run_comments_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "run_comments_run_id_runs_id_fk": { + "name": "run_comments_run_id_runs_id_fk", + "tableFrom": "run_comments", + "tableTo": "runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "run_comments_user_id_users_id_fk": { + "name": "run_comments_user_id_users_id_fk", + "tableFrom": "run_comments", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.run_events": { + "name": "run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "run_events_run_seq_uq": { + "name": "run_events_run_seq_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "run_events_run_id_runs_id_fk": { + "name": "run_events_run_id_runs_id_fk", + "tableFrom": "run_events", + "tableTo": "runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "run_events_step_id_run_steps_id_fk": { + "name": "run_events_step_id_run_steps_id_fk", + "tableFrom": "run_events", + "tableTo": "run_steps", + "columnsFrom": ["step_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.run_steps": { + "name": "run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "iteration": { + "name": "iteration", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "agent_ref": { + "name": "agent_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_id": { + "name": "model_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "executor_session_id": { + "name": "executor_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "run_steps_uq": { + "name": "run_steps_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "phase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "iteration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "run_steps_run_id_runs_id_fk": { + "name": "run_steps_run_id_runs_id_fk", + "tableFrom": "run_steps", + "tableTo": "runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.runs": { + "name": "runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "template_version_id": { + "name": "template_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "faber_id": { + "name": "faber_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "executor_id": { + "name": "executor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "params_snapshot": { + "name": "params_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_resolution": { + "name": "model_resolution", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resource_manifest": { + "name": "resource_manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"mcpServers\":[],\"skills\":[]}'::jsonb" + }, + "budget": { + "name": "budget", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "usage_totals": { + "name": "usage_totals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "next_event_seq": { + "name": "next_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "workspace_ref": { + "name": "workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cancel_requested": { + "name": "cancel_requested", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agent_bindings": { + "name": "agent_bindings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "work_branch": { + "name": "work_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "runs_task_number_uq": { + "name": "runs_task_number_uq", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "runs_project_idx": { + "name": "runs_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "runs_task_id_tasks_id_fk": { + "name": "runs_task_id_tasks_id_fk", + "tableFrom": "runs", + "tableTo": "tasks", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "runs_project_id_projects_id_fk": { + "name": "runs_project_id_projects_id_fk", + "tableFrom": "runs", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "runs_template_version_id_template_versions_id_fk": { + "name": "runs_template_version_id_template_versions_id_fk", + "tableFrom": "runs", + "tableTo": "template_versions", + "columnsFrom": ["template_version_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "runs_faber_id_fabri_id_fk": { + "name": "runs_faber_id_fabri_id_fk", + "tableFrom": "runs", + "tableTo": "fabri", + "columnsFrom": ["faber_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "runs_created_by_users_id_fk": { + "name": "runs_created_by_users_id_fk", + "tableFrom": "runs", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_type_id": { + "name": "task_type_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "agent_overrides": { + "name": "agent_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_org_id_orgs_id_fk": { + "name": "tasks_org_id_orgs_id_fk", + "tableFrom": "tasks", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_task_type_id_task_types_id_fk": { + "name": "tasks_task_type_id_task_types_id_fk", + "tableFrom": "tasks", + "tableTo": "task_types", + "columnsFrom": ["task_type_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_latest_run_id_runs_id_fk": { + "name": "tasks_latest_run_id_runs_id_fk", + "tableFrom": "tasks", + "tableTo": "runs", + "columnsFrom": ["latest_run_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_created_by_users_id_fk": { + "name": "tasks_created_by_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secrets": { + "name": "secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "secrets_org_id_orgs_id_fk": { + "name": "secrets_org_id_orgs_id_fk", + "tableFrom": "secrets", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "secrets_created_by_users_id_fk": { + "name": "secrets_created_by_users_id_fk", + "tableFrom": "secrets", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_usage": { + "name": "token_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "model_id": { + "name": "model_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(12, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "token_usage_project_time_idx": { + "name": "token_usage_project_time_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "token_usage_run_idx": { + "name": "token_usage_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "token_usage_org_id_orgs_id_fk": { + "name": "token_usage_org_id_orgs_id_fk", + "tableFrom": "token_usage", + "tableTo": "orgs", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "token_usage_project_id_projects_id_fk": { + "name": "token_usage_project_id_projects_id_fk", + "tableFrom": "token_usage", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "token_usage_run_id_runs_id_fk": { + "name": "token_usage_run_id_runs_id_fk", + "tableFrom": "token_usage", + "tableTo": "runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "token_usage_step_id_run_steps_id_fk": { + "name": "token_usage_step_id_run_steps_id_fk", + "tableFrom": "token_usage", + "tableTo": "run_steps", + "columnsFrom": ["step_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "token_usage_model_id_models_id_fk": { + "name": "token_usage_model_id_models_id_fk", + "tableFrom": "token_usage", + "tableTo": "models", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 189e1e8..7aee3af 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1784843355774, "tag": "0007_provider-credentials", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1785158387829, + "tag": "0008_agent-overrides", + "breakpoints": true } ] } diff --git a/packages/db/src/schema/runs.ts b/packages/db/src/schema/runs.ts index f877bb6..fc0b763 100644 --- a/packages/db/src/schema/runs.ts +++ b/packages/db/src/schema/runs.ts @@ -36,6 +36,12 @@ export const tasks = pgTable("tasks", { .references(() => taskTypes.id), title: text("title").notNull(), params: jsonb("params").$type>().notNull(), + // raw submit-time per-slot agent overrides, kept so a retry can re-resolve + // without losing the user's deliberate slot choices (ADR-0014); {} = none + agentOverrides: jsonb("agent_overrides") + .$type>() + .notNull() + .default({}), latestRunId: uuid("latest_run_id").references((): AnyPgColumn => runs.id), createdBy: uuid("created_by") .notNull() From a428dcd342fde077f44b9fc3a842c1545f0298a8 Mon Sep 17 00:00:00 2001 From: John Hu Date: Mon, 27 Jul 2026 21:27:39 +0800 Subject: [PATCH 3/8] feat(api): retry re-resolves bindings, models, manifest, and budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retry copied the previous run's frozen resolution verbatim, which made it structurally unable to heal configuration failures: after fixing grants/registry/credentials the retried run re-billed the whole pipeline from checkout and died at the same step. Per ADR-0014 retry is now a re-submission of the pinned task — template version and params snapshot stay pinned, everything configuration-derived re-resolves through resolveRunPlan, a helper shared with submit so the two paths cannot drift. With unchanged configuration re-resolution is deterministic, so transient-failure retries behave exactly as before. Retry also gains submit parity it silently lacked: the quota hard-stop, repo-ref ownership check, a transactional insert (run + latestRunId together), and an audit payload naming the source run. The narrow assertResolutionCredentialed gate (ADR-0013 amendment 1) is retired — full resolution enforces the same credential check and every stronger one. Submit error codes retry can now surface (executor_unavailable and siblings) join both locales' error catalogs. --- CHANGELOG.md | 4 + apps/api/src/routes/execution.ts | 150 ++++++++++++------ .../src/test/execution.integration.test.ts | 100 +++++++++++- .../src/test/providers.integration.test.ts | 5 +- docs/design/01-domain-model.md | 2 +- docs/design/04-execution-runtime.md | 4 + docs/design/05-api-and-auth.md | 2 +- docs/manual/en/02-concepts.md | 2 +- docs/manual/en/03-running-tasks.md | 2 +- docs/manual/zh-CN/02-concepts.md | 2 +- docs/manual/zh-CN/03-running-tasks.md | 2 +- packages/i18n/locales/en/errors.json | 6 + packages/i18n/locales/zh-CN/errors.json | 6 + packages/orchestration/src/resolve.ts | 54 +------ 14 files changed, 230 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb801da..cf3ba00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,10 @@ All notable changes to Agrippa are documented here. The format follows ### Changed +- **Task retry re-resolves against current configuration** ([ADR-0014](docs/adr/0014-retry-re-resolves-configuration.md)) — retry used to copy the previous run's frozen bindings verbatim, so a configuration failure (a model the auth mode can't serve, a revoked grant, a deleted credential) could never be healed by fixing the configuration and retrying: the retried run re-billed the whole pipeline from checkout and died identically. Retry is now a re-submission of the pinned task: + - *Template version and params snapshot stay pinned; everything configuration-derived re-resolves.* Agent bindings, per-slot model resolution, the resource manifest, the budget, quota headroom, and repo-ref ownership all re-derive through the same helper submit uses — with unchanged configuration the result is deterministically identical, so transient-failure retries behave as before. Configuration that still cannot satisfy a slot fails fast at the endpoint with submit's error codes (now localized, including `executor_unavailable`), never mid-run. + - *Submit-time agent overrides persist.* The `agents` slot overrides picked on the submit form land on `tasks.agent_overrides` (migration 0008) and re-apply at retry, so a deliberate executor choice survives re-resolution; tasks predating the column retry with template defaults. + - *`assertResolutionCredentialed` is retired* — the narrow credential re-check (ADR-0013 amendment 1) is subsumed by the full resolution gate. Retry also gained submit's transactional insert and an audit payload naming the source run. - **`approvals` table → `checkpoints`** (data-preserving rename) with `kind`, `iteration`, and a structured `response`; `run_steps` and `artifacts` gain `iteration`; `runs` gain `agent_bindings` and `work_branch`; new `run_comments` table (migration `0005_requirement-delivery`). Engine events rename `approval.required/decided` → `checkpoint.required/decided` and add `loop.*`, `branch.*`, `pr.opened`, `comment.added`. - **Demo mode hardening** — when `AGRIPPA_EXECUTOR=fake`, every agent slot binds to the demo executor (a demo install must never silently route a slot to a real, key-consuming engine), `AGRIPPA_SCM=fake` fabricates branch/push/PR, and the demo executor is loop-round-aware (questions/findings in round 1, clean in round 2) so the delivery workflow demos end-to-end without tokens. diff --git a/apps/api/src/routes/execution.ts b/apps/api/src/routes/execution.ts index 7ccd8c5..f8843b0 100644 --- a/apps/api/src/routes/execution.ts +++ b/apps/api/src/routes/execution.ts @@ -32,9 +32,9 @@ import { } from "@agrippa/db"; import { appendRunEvent as allocateRunEvent, - assertResolutionCredentialed, authorizeResources, buildParamsValidator, + type CompiledTemplate, decideCheckpoint, flattenPhases, resolveAgentBindings, @@ -54,6 +54,45 @@ import { assertProjectRole, requireProjectRole } from "../middleware/rbac"; const DEFAULT_EXECUTOR = process.env.AGRIPPA_EXECUTOR ?? "claude-agent-sdk"; +/** + * Everything a new run derives from CURRENT project configuration: the + * resource manifest, per-slot agent bindings and model resolution, and the + * budget off the pinned compiled spec. Shared by submit and retry so the two + * paths cannot drift — a retry is a re-submission of the pinned task + * (ADR-0014); only the template version and params snapshot stay pinned. + */ +async function resolveRunPlan( + db: AppEnv["Variables"]["db"], + projectId: string, + taskType: { defaultFaberId: string }, + compiled: CompiledTemplate, + overrides: Record, +) { + const skillRows = await db.select({ id: skills.id, slug: skills.slug }).from(skills); + const mcpRows = await db.select({ id: mcpServers.id, slug: mcpServers.slug }).from(mcpServers); + // required grants enforced; optional resources pinned only when granted + const resourceManifest = await authorizeResources(db, projectId, compiled, { + skillIdBySlug: new Map(skillRows.map((s) => [s.slug, s.id])), + mcpIdBySlug: new Map(mcpRows.map((m) => [m.slug, m.id])), + }); + // every agent slot resolves to a concrete faber + executor (per-slot + // provider-filtered models, checked against the executors the deployment's + // workers actually registered) and freezes onto the run + const agentResolution = await resolveAgentBindings( + db, + projectId, + compiled, + { faberId: taskType.defaultFaberId, executorId: DEFAULT_EXECUTOR }, + overrides, + { registeredExecutors: await liveExecutorIds(db) }, + ); + return { + resourceManifest, + agentResolution, + budget: compiled.spec.budgets as unknown as Record, + }; +} + async function loadRunScoped( c: { var: AppEnv["Variables"] }, runId: string, @@ -308,25 +347,12 @@ export const executionRoutes = new Hono() // every repoRef must reference a connection owned by this project await verifyRepoRefs(db, projectId, compiled.spec.inputs, parsed.data); - const skillRows = await db.select({ id: skills.id, slug: skills.slug }).from(skills); - const mcpRows = await db - .select({ id: mcpServers.id, slug: mcpServers.slug }) - .from(mcpServers); - // required grants enforced; optional resources pinned only when granted - const resourceManifest = await authorizeResources(db, projectId, compiled, { - skillIdBySlug: new Map(skillRows.map((s) => [s.slug, s.id])), - mcpIdBySlug: new Map(mcpRows.map((m) => [m.slug, m.id])), - }); - // every agent slot resolves to a concrete faber + executor here (with - // per-slot provider-filtered models and a check against the executors - // the deployment's workers actually registered) and freezes onto the run - const agentResolution = await resolveAgentBindings( + const { resourceManifest, agentResolution, budget } = await resolveRunPlan( db, projectId, + taskType, compiled, - { faberId: taskType.defaultFaberId, executorId: DEFAULT_EXECUTOR }, input.agents ?? {}, - { registeredExecutors: await liveExecutorIds(db) }, ); const { task, run } = await db.transaction(async (tx) => { @@ -356,7 +382,7 @@ export const executionRoutes = new Hono() paramsSnapshot: parsed.data, modelResolution: agentResolution.modelResolution, resourceManifest, - budget: compiled.spec.budgets as unknown as Record, + budget, createdBy: user.id, }) .returning(); @@ -442,49 +468,69 @@ export const executionRoutes = new Hono() throw AppError.conflict("run_active", "The latest run has not finished"); } - // the frozen resolution is copied verbatim below, so re-assert that its - // project-only provider credentials still exist — a credential deleted - // since the last run must fail here, not as an auth error mid-run + // deliberately no taskType.enabled check: disabling blocks new catalog + // submissions, not retries of a task that already exists (ADR-0014) + const [taskType] = await db.select().from(taskTypes).where(eq(taskTypes.id, task.taskTypeId)); + if (!taskType) throw AppError.notFound("Task type"); + const [version] = await db + .select() + .from(templateVersions) + .where(eq(templateVersions.id, latest.templateVersionId)); + if (!version) throw AppError.notFound("Template version"); + const compiled = upgradeCompiledTemplate(version.compiled); + + // a retry burns budget like any run — same hard-stop as submit + await assertQuotaHeadroom(db, task.projectId); + try { - await assertResolutionCredentialed( + // ADR-0014: the template version and params snapshot stay pinned; + // everything derived from project configuration re-resolves, so a + // config fix (grants, registry, credentials) heals the task here + // instead of re-billing a full run into the same failure + await verifyRepoRefs(db, task.projectId, compiled.spec.inputs, latest.paramsSnapshot); + const { resourceManifest, agentResolution, budget } = await resolveRunPlan( db, task.projectId, - latest.modelResolution as Record, - latest.agentBindings as Record | null, - latest.executorId, + taskType, + compiled, + task.agentOverrides ?? {}, ); + + const run = await db.transaction(async (tx) => { + const [run] = await tx + .insert(runs) + .values({ + taskId: task.id, + projectId: task.projectId, + number: latest.number + 1, + templateVersionId: latest.templateVersionId, + faberId: agentResolution.primary.faberId, + executorId: agentResolution.primary.executorId, + agentBindings: agentResolution.bindings, + paramsSnapshot: latest.paramsSnapshot, + modelResolution: agentResolution.modelResolution, + resourceManifest, + budget, + createdBy: c.var.user.id, + }) + .returning(); + if (!run) throw new Error("run insert failed"); + await tx.update(tasks).set({ latestRunId: run.id }).where(eq(tasks.id, task.id)); + return run; + }); + await audit(c, { + action: "task.retry", + resourceType: "run", + resourceId: run.id, + projectId: task.projectId, + payload: { fromRunId: latest.id, taskId: task.id }, + }); + await c.var.queue?.enqueueRun(run.id); + return c.json({ runId: run.id, number: run.number }, 202); } catch (err) { if (err instanceof SubmitError) throw new AppError(err.code, 400, err.message, err.details); throw err; } - - const [run] = await db - .insert(runs) - .values({ - taskId: task.id, - projectId: task.projectId, - number: latest.number + 1, - templateVersionId: latest.templateVersionId, // pinned — retries never re-resolve - faberId: latest.faberId, - executorId: latest.executorId, - agentBindings: latest.agentBindings, - paramsSnapshot: latest.paramsSnapshot, - modelResolution: latest.modelResolution, - resourceManifest: latest.resourceManifest, - budget: latest.budget, - createdBy: c.var.user.id, - }) - .returning(); - if (!run) throw new Error("run insert failed"); - await db.update(tasks).set({ latestRunId: run.id }).where(eq(tasks.id, task.id)); - await audit(c, { - action: "task.retry", - resourceType: "run", - resourceId: run.id, - projectId: task.projectId, - }); - await c.var.queue?.enqueueRun(run.id); - return c.json({ runId: run.id, number: run.number }, 202); }) // ── Runs ──────────────────────────────────────────────────────────────────── diff --git a/apps/api/src/test/execution.integration.test.ts b/apps/api/src/test/execution.integration.test.ts index 9144c6e..a8dfbb3 100644 --- a/apps/api/src/test/execution.integration.test.ts +++ b/apps/api/src/test/execution.integration.test.ts @@ -11,7 +11,7 @@ import { InProcessEventBus, silentLogger, } from "@agrippa/orchestration"; -import { eq } from "drizzle-orm"; +import { desc, eq } from "drizzle-orm"; import type { App } from "../app"; import { createApp } from "../app"; import { freshTestDb, jsonOf, postgresAvailable, signUp, type TestClient } from "./helpers"; @@ -422,4 +422,102 @@ describe.skipIf(!dbUp)("execution api (submit → engine → approve → artifac ); expect(task.runs.map((r) => r.number)).toEqual([2, 1]); }); + + it("retry re-resolves against current configuration (ADR-0014)", async () => { + // no worker in this fixture — mark queued runs terminal so retry is legal + await db.update(runs).set({ status: "failed" }).where(eq(runs.taskId, taskId)); + const [prev] = await db + .select() + .from(runs) + .where(eq(runs.taskId, taskId)) + .orderBy(desc(runs.number)) + .limit(1); + const flatten = (resolution: Record | null | undefined) => + Object.values(resolution ?? {}).flatMap((slot) => + Object.values(slot as Record), + ); + // every run so far resolved anthropic — no dashscope credential existed + expect(flatten(prev?.modelResolution).some((e) => e.provider === "anthropic")).toBe(true); + + // adding a credential makes dashscope the ranked provider (ADR-0013's + // credentialed-first consequence) — a retry must pick that up + await admin.request(`/api/v1/projects/${projectId}/providers`, { + method: "POST", + json: { provider: "dashscope", apiKey: "sk-bailian-retry-test" }, + }); + const res = await admin.request(`/api/v1/tasks/${taskId}/retry`, { method: "POST" }); + expect(res.status).toBe(202); + const { runId: rerunId } = await jsonOf<{ runId: string }>(res); + const [rerun] = await db.select().from(runs).where(eq(runs.id, rerunId)); + + // pinned: template version and params snapshot; re-derived: the resolution + expect(rerun?.templateVersionId).toBe(prev?.templateVersionId as string); + expect(rerun?.paramsSnapshot).toEqual(prev?.paramsSnapshot ?? {}); + const entries = flatten(rerun?.modelResolution); + expect(entries.length).toBeGreaterThan(0); + for (const entry of entries) expect(entry.provider).toBe("dashscope"); + + await admin.request(`/api/v1/projects/${projectId}/providers/dashscope`, { + method: "DELETE", + }); + }); + + it("retry preserves the user's submit-time agent overrides", async () => { + const types = await jsonOf>( + await admin.request("/api/v1/scenarios/software-development/task-types"), + ); + const rdTypeId = types.find((t) => t.slug === "requirement-delivery")?.id as string; + const submitted = await admin.request(`/api/v1/projects/${projectId}/tasks`, { + method: "POST", + json: { + taskTypeId: rdTypeId, + title: "Override survives retry", + params: { requirement: "Do the thing", repo: { repoConnectionId } }, + // the template pins codex-cli for the reviewer; the user chose otherwise + agents: { reviewer: { executorId: "claude-agent-sdk" } }, + }, + }); + expect(submitted.status).toBe(202); + const { taskId: rdTaskId } = await jsonOf<{ taskId: string }>(submitted); + + await db.update(runs).set({ status: "failed" }).where(eq(runs.taskId, rdTaskId)); + const retried = await admin.request(`/api/v1/tasks/${rdTaskId}/retry`, { method: "POST" }); + expect(retried.status).toBe(202); + const { runId: rerunId } = await jsonOf<{ runId: string }>(retried); + const [rerun] = await db.select().from(runs).where(eq(runs.id, rerunId)); + expect(rerun?.agentBindings.reviewer?.executorId).toBe("claude-agent-sdk"); + }); + + it("retry fails fast when current configuration cannot satisfy the template", async () => { + await db.update(runs).set({ status: "failed" }).where(eq(runs.taskId, taskId)); + + // strip every grant — the manifest re-check fires before model resolution + await admin.request(`/api/v1/projects/${projectId}/grants`, { method: "PUT", json: [] }); + const ungranted = await admin.request(`/api/v1/tasks/${taskId}/retry`, { method: "POST" }); + expect(ungranted.status).toBe(400); + expect((await jsonOf<{ code: string }>(ungranted)).code).toBe("skill_not_granted"); + + const models = await jsonOf>(await admin.request("/api/v1/models")); + const skillRows = await jsonOf>(await admin.request("/api/v1/skills")); + await admin.request(`/api/v1/projects/${projectId}/grants`, { + method: "PUT", + json: [ + ...models.map((m) => ({ resourceType: "model", resourceId: m.id })), + ...skillRows.map((s) => ({ resourceType: "skill", resourceId: s.id })), + ], + }); + + // a retry burns budget like any run — the quota hard-stop applies + await admin.request(`/api/v1/projects/${projectId}/quota`, { + method: "PUT", + json: { tokenLimit: 1, hardStop: true }, + }); + const blocked = await admin.request(`/api/v1/tasks/${taskId}/retry`, { method: "POST" }); + expect(blocked.status).toBe(400); + expect((await jsonOf<{ code: string }>(blocked)).code).toBe("quota_exhausted"); + await admin.request(`/api/v1/projects/${projectId}/quota`, { + method: "PUT", + json: { tokenLimit: null, hardStop: true }, + }); + }); }); diff --git a/apps/api/src/test/providers.integration.test.ts b/apps/api/src/test/providers.integration.test.ts index 19885d2..58843db 100644 --- a/apps/api/src/test/providers.integration.test.ts +++ b/apps/api/src/test/providers.integration.test.ts @@ -260,11 +260,12 @@ describe.skipIf(!dbUp)("provider credentials (project settings → submit gate)" gatedTaskId = taskId; }); - it("retry re-checks the credential gate against the frozen resolution", async () => { + it("retry re-checks the credential gate against current configuration", async () => { // no worker in this fixture — mark the queued run terminal so retry is legal await db.update(runs).set({ status: "failed" }).where(eq(runs.taskId, gatedTaskId)); - // deleting the credential must block retry, not fail mid-run on auth + // retry re-resolves in full (ADR-0014); with dashscope-only grants the + // deleted credential must block retry here, not fail mid-run on auth await admin.request(`/api/v1/projects/${gatedProjectId}/providers/dashscope`, { method: "DELETE", }); diff --git a/docs/design/01-domain-model.md b/docs/design/01-domain-model.md index 2202780..45542d8 100644 --- a/docs/design/01-domain-model.md +++ b/docs/design/01-domain-model.md @@ -219,7 +219,7 @@ audit_logs (id pk, org_id fk, project_id fk null, - **Why `run_events` + `run_steps` both**: events give a replayable, gap-free timeline (SSE resume via `Last-Event-ID` = per-run `seq`); steps give cheap queryability (current status, per-step usage) without scanning events. The engine writes the event first, then updates the projection. - **Why `params_snapshot` on runs** when `tasks.params` exists: a retry may happen after the task type's template was republished with different inputs; the run must be self-contained and auditable. -- **Why `model_resolution` frozen at run start**: role→tier→model resolution depends on project grants, which can change mid-run; freezing makes runs reproducible and usage attribution unambiguous. +- **Why `model_resolution` frozen at run start**: role→tier→model resolution depends on project grants, which can change mid-run; freezing makes runs reproducible and usage attribution unambiguous. The freeze is per **run**, never per task — a retry re-resolves against current configuration and freezes its own resolution (ADR-0014). - **Why `token_usage.attempt`**: a retried step re-incurs cost; rows keyed by `(run_id, step_id, attempt)` let the budget meter sum persisted totals on resume without double-counting a partially-executed attempt (the attempt's rows are written incrementally and summed as-is — cost is real even when the attempt failed). - **Why `mcp_servers.config_revision`**: MCP config is mutable head state (no full versioning in M1 — configs are small and secrets rotate); runs record the revision they resolved so audits can detect drift. - **Storage**: artifacts ≤ 64 KB are stored `inline` (jsonb/text); larger ones go to a disk-backed store at `storage_ref` (a Docker volume path in M1; the indirection allows S3 later). diff --git a/docs/design/04-execution-runtime.md b/docs/design/04-execution-runtime.md index 9a9d831..a15963f 100644 --- a/docs/design/04-execution-runtime.md +++ b/docs/design/04-execution-runtime.md @@ -79,6 +79,10 @@ enforce output contract (required artifacts present? latest iteration wins) finalize: usage_totals, workspace cleanup, terminal event ``` +### Run-level retry (re-submission) + +Task retry (`POST /tasks/:id/retry`) creates run #N+1 as a **re-submission of the pinned task** (ADR-0014): the template version and params snapshot are copied, while everything derived from project configuration — agent bindings, per-slot model resolution, the resource manifest, the budget, quota headroom, repo-ref ownership — re-resolves through the same helper submit uses, applying the task's persisted submit-time agent overrides. Unchanged configuration reproduces the previous bindings deterministically; changed configuration is picked up, so a config fix heals a failed task instead of re-billing a doomed run. Configuration that still cannot satisfy a slot fails at the endpoint with submit's error codes, never mid-run. This is distinct from the *step-level* retry below, which resumes inside one run. + ### Resumability (step-granular) Steps are the idempotency unit. On retry/resume, the engine loads `run_steps`, **skips succeeded steps**, and re-executes the first non-terminal step: diff --git a/docs/design/05-api-and-auth.md b/docs/design/05-api-and-auth.md index c2c1e09..b9d6d91 100644 --- a/docs/design/05-api-and-auth.md +++ b/docs/design/05-api-and-auth.md @@ -78,7 +78,7 @@ GET /task-types/:id # includes compiled input schema → f ``` POST /projects/:id/tasks # {taskTypeId, title, params, agents?} → 202 {taskId, runId} GET /projects/:id/tasks?status=&taskType= -GET /tasks/:id POST /tasks/:id/retry # → new run (bindings copied) +GET /tasks/:id POST /tasks/:id/retry # → new run (re-resolved vs current config, ADR-0014) GET /runs/:id # embeds a viewer-scoped template plan (phases/loops/checkpoints/budgets/modelRoles — # no prompts), per-slot agent metadata, and all checkpoint rows with decider names GET /runs/:id/steps # each row carries iteration + usage {costUsd, tokens} diff --git a/docs/manual/en/02-concepts.md b/docs/manual/en/02-concepts.md index 950e789..60e73ca 100644 --- a/docs/manual/en/02-concepts.md +++ b/docs/manual/en/02-concepts.md @@ -4,7 +4,7 @@ **Scenario & task type** — the catalog is organized by work scenario (project management, software development, test & verification). A *task type* is one concrete thing you can ask for — "Bug Localization & Fix", "Test Plan" — and binds a scenario to an orchestration template and a default Faber. -**Task & run** — a *task* is your submission: a task type, a title, and parameters. Executing it produces a *run*; retrying a finished task produces run #2, #3… Each run is pinned to the exact template version that was current at submission, so later template changes never affect it. +**Task & run** — a *task* is your submission: a task type, a title, and parameters. Executing it produces a *run*; retrying a finished task produces run #2, #3… Each run is pinned to the exact template version that was current at submission, so later template changes never affect it; agents and models, by contrast, bind to the project configuration current at each run. **Step** — the atomic unit inside a run: one agent invocation (or one system action such as checking out the repository). Steps are what you see on the run timeline, and they're the platform's unit of retry and crash recovery. diff --git a/docs/manual/en/03-running-tasks.md b/docs/manual/en/03-running-tasks.md index 873c76f..d58a3eb 100644 --- a/docs/manual/en/03-running-tasks.md +++ b/docs/manual/en/03-running-tasks.md @@ -42,5 +42,5 @@ For delivery workflows, the final branch contains one platform-created snapshot ## Cancel, retry, and failures - **Cancel** (members+) stops a run — immediately for queued/paused runs, at the next safe point for executing ones. -- **Retry** appears on finished runs: it creates a fresh run (#2, #3…) with the same parameters, pinned to the same template version. +- **Retry** appears on finished runs: it creates a fresh run (#2, #3…) with the same parameters, pinned to the same template version, but binding agents and models against **today's** project configuration (grants, credentials, engines) — fix the configuration and retry to heal a run that failed on it. The run re-executes from checkout, so earlier answers and approvals are asked again. - **Failures** always carry a reason: `budget_exceeded` (run budget or project quota), `approval_rejected`, `contract_violation` (a required artifact was never produced), `timed_out`, or a step error. Partial progress — steps, outputs, artifacts produced before the failure — remains visible and downloadable. diff --git a/docs/manual/zh-CN/02-concepts.md b/docs/manual/zh-CN/02-concepts.md index 9b050b6..d1725bb 100644 --- a/docs/manual/zh-CN/02-concepts.md +++ b/docs/manual/zh-CN/02-concepts.md @@ -4,7 +4,7 @@ **场景与任务类型** —— 任务目录按工作场景组织(项目管理、软件研发、测试验证)。*任务类型*是一件可以具体提交的事——如「缺陷定位与修复」「测试计划」——它把场景、编排模板与默认硅基人绑定在一起。 -**任务与执行(Task / Run)** —— *任务*是你的一次提交:任务类型 + 标题 + 参数。执行任务产生一次*执行*;对已结束的任务重试会产生第 2、3 次执行。每次执行都固定在提交那一刻的模板版本上,之后的模板变更不会影响它。 +**任务与执行(Task / Run)** —— *任务*是你的一次提交:任务类型 + 标题 + 参数。执行任务产生一次*执行*;对已结束的任务重试会产生第 2、3 次执行。每次执行都固定在提交那一刻的模板版本上,之后的模板变更不会影响它;而智能体与模型则按每次执行时的项目配置重新绑定。 **步骤(Step)** —— 执行内部的原子单元:一次智能体调用(或一次系统动作,如检出代码仓库)。步骤构成执行时间线,也是平台重试与崩溃恢复的基本单位。 diff --git a/docs/manual/zh-CN/03-running-tasks.md b/docs/manual/zh-CN/03-running-tasks.md index 1d9f415..f2f4f67 100644 --- a/docs/manual/zh-CN/03-running-tasks.md +++ b/docs/manual/zh-CN/03-running-tasks.md @@ -42,5 +42,5 @@ ## 取消、重试与失败 - **取消**(成员及以上):排队/暂停中的执行立即取消;执行中的在下一个安全点停止。 -- **重试**:出现在已结束的执行上——以相同参数创建新的执行(第 2、3 次……),固定在同一模板版本。 +- **重试**:出现在已结束的执行上——以相同参数创建新的执行(第 2、3 次……),固定在同一模板版本,但智能体与模型会按**当前**项目配置(授权、凭证、执行引擎)重新解析——修好配置后重试,即可恢复因配置而失败的任务。整个执行会从检出重新开始,此前的回答与审批会再次询问。 - **失败**总是带有原因:`budget_exceeded`(执行预算或项目配额)、`approval_rejected`、`contract_violation`(必需产出物未产出)、`timed_out`、或某步骤的错误。失败前已完成的步骤、输出与产出物仍然可见、可下载。 diff --git a/packages/i18n/locales/en/errors.json b/packages/i18n/locales/en/errors.json index eeae45c..86dcbb8 100644 --- a/packages/i18n/locales/en/errors.json +++ b/packages/i18n/locales/en/errors.json @@ -18,6 +18,12 @@ "template_unpublished": "This task type has no published template", "no_models_granted": "No models are granted to this project", "model_unresolvable": "No granted model satisfies a required role", + "executor_unavailable": "This executor is not available in this deployment", + "executor_capability": "This executor cannot run a step the template assigns to it", + "executor_unknown": "Unknown executor", + "faber_unknown": "This Faber is not registered or not active", + "slot_unknown": "Unknown agent slot", + "slot_not_overridable": "This agent slot is fixed by the template", "provider_credential_required": "This provider requires a project credential — add one in project settings", "provider_exists": "This provider already has a credential — rotate or remove it", "base_url_invalid": "The base URL is not an acceptable provider endpoint", diff --git a/packages/i18n/locales/zh-CN/errors.json b/packages/i18n/locales/zh-CN/errors.json index 1733f57..1dd65a0 100644 --- a/packages/i18n/locales/zh-CN/errors.json +++ b/packages/i18n/locales/zh-CN/errors.json @@ -18,6 +18,12 @@ "template_unpublished": "该任务类型尚无已发布的模板", "no_models_granted": "项目尚未授权任何模型", "model_unresolvable": "没有已授权的模型满足所需角色", + "executor_unavailable": "当前部署中没有可用的该执行引擎", + "executor_capability": "该执行引擎无法运行模板分配给它的步骤", + "executor_unknown": "未知的执行引擎", + "faber_unknown": "该 Faber 未注册或未激活", + "slot_unknown": "未知的智能体槽位", + "slot_not_overridable": "该智能体槽位由模板固定,不可覆盖", "provider_credential_required": "该模型服务商需要项目级凭证,请在项目设置中添加", "provider_exists": "该服务商已有凭证,请轮换或先删除", "base_url_invalid": "接入地址不是可接受的服务商端点", diff --git a/packages/orchestration/src/resolve.ts b/packages/orchestration/src/resolve.ts index ac7ef40..e9db59c 100644 --- a/packages/orchestration/src/resolve.ts +++ b/packages/orchestration/src/resolve.ts @@ -291,56 +291,10 @@ export async function resolveModelRoles( }); } -/** - * Re-assert that every provider a frozen model resolution depends on still - * has its required project credential. Retries copy the resolution verbatim, - * so a credential deleted between runs would otherwise surface only as an - * auth failure mid-run instead of an actionable submit error. Handles both - * resolution shapes (legacy flat role → entry and slot-keyed) and skips - * slots bound to the fake or an uncataloged executor — those resolved with - * no credential gating in the first place. - */ -export async function assertResolutionCredentialed( - db: Db, - projectId: string, - modelResolution: Record, - agentBindings: Record | null, - executorId: string, -): Promise { - const required = new Set(); - const collect = (entries: ModelResolutionEntry[]): void => { - for (const entry of entries) { - if (providerAuthPolicy(entry.provider) === "project") required.add(entry.provider); - } - }; - const values = Object.values(modelResolution ?? {}); - const flat = values.every( - (v) => v !== null && typeof v === "object" && "providerModelId" in (v as object), - ); - if (flat) { - if (isCredentialGatedExecutor(executorId)) collect(values as ModelResolutionEntry[]); - } else { - for (const [slot, resolution] of Object.entries(modelResolution ?? {})) { - const execId = agentBindings?.[slot]?.executorId ?? executorId; - if (!isCredentialGatedExecutor(execId)) continue; - collect(Object.values(resolution as Record)); - } - } - if (required.size === 0) return; - const rows = await db - .select({ provider: providerCredentials.provider }) - .from(providerCredentials) - .where(eq(providerCredentials.projectId, projectId)); - const have = new Set(rows.map((r) => r.provider)); - const missing = [...required].filter((p) => !have.has(p)); - if (missing.length > 0) { - throw new SubmitError( - "provider_credential_required", - `Provider ${missing.join(" and ")} requires a project credential (project settings → providers)`, - { providers: missing }, - ); - } -} +// assertResolutionCredentialed used to live here: the retry endpoint's narrow +// re-check that a frozen resolution's project credentials still existed. +// ADR-0014 made retry re-resolve in full, so resolveAgentBindings now enforces +// that gate (and every stronger one) on the retry path too. export type AgentBindingResolution = { /** slot → concrete binding, frozen into runs.agent_bindings. */ From 6194280d71e14fb49f841319213ba369c7b2ff49 Mon Sep 17 00:00:00 2001 From: John Hu Date: Mon, 27 Jul 2026 21:28:44 +0800 Subject: [PATCH 4/8] feat(web): confirm retry and surface its failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retry now re-resolves against current configuration (ADR-0014), which gives the button two properties worth stating before the click: the new run starts over from checkout — previously answered questions and approved plans are asked again — and it binds agents/models to today's project configuration. A ConfirmDialog says both. Re-resolution can also reject the way submit does (grants, credentials, quota), so the mutation gains a localized error toast instead of failing silently. The run-page actions are additionally hidden from viewers, who could only ever collect a 403 from the member-gated endpoints. --- CHANGELOG.md | 1 + apps/web/src/pages/RunDetailPage.tsx | 32 ++++++++++++++++++--------- docs/design/06-frontend.md | 2 +- packages/i18n/locales/en/runs.json | 3 +++ packages/i18n/locales/zh-CN/runs.json | 3 +++ 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3ba00..b32082b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ All notable changes to Agrippa are documented here. The format follows - *Template version and params snapshot stay pinned; everything configuration-derived re-resolves.* Agent bindings, per-slot model resolution, the resource manifest, the budget, quota headroom, and repo-ref ownership all re-derive through the same helper submit uses — with unchanged configuration the result is deterministically identical, so transient-failure retries behave as before. Configuration that still cannot satisfy a slot fails fast at the endpoint with submit's error codes (now localized, including `executor_unavailable`), never mid-run. - *Submit-time agent overrides persist.* The `agents` slot overrides picked on the submit form land on `tasks.agent_overrides` (migration 0008) and re-apply at retry, so a deliberate executor choice survives re-resolution; tasks predating the column retry with template defaults. - *`assertResolutionCredentialed` is retired* — the narrow credential re-check (ADR-0013 amendment 1) is subsumed by the full resolution gate. Retry also gained submit's transactional insert and an audit payload naming the source run. + - *The Retry button says what it does.* A confirm dialog states that the new run starts over from checkout under current configuration and re-asks earlier answers/approvals; rejections surface as localized toasts; the run-page actions are hidden from viewers, who could only collect a 403. - **`approvals` table → `checkpoints`** (data-preserving rename) with `kind`, `iteration`, and a structured `response`; `run_steps` and `artifacts` gain `iteration`; `runs` gain `agent_bindings` and `work_branch`; new `run_comments` table (migration `0005_requirement-delivery`). Engine events rename `approval.required/decided` → `checkpoint.required/decided` and add `loop.*`, `branch.*`, `pr.opened`, `comment.added`. - **Demo mode hardening** — when `AGRIPPA_EXECUTOR=fake`, every agent slot binds to the demo executor (a demo install must never silently route a slot to a real, key-consuming engine), `AGRIPPA_SCM=fake` fabricates branch/push/PR, and the demo executor is loop-round-aware (questions/findings in round 1, clean in round 2) so the delivery workflow demos end-to-end without tokens. diff --git a/apps/web/src/pages/RunDetailPage.tsx b/apps/web/src/pages/RunDetailPage.tsx index dd6b6d0..b70f0a8 100644 --- a/apps/web/src/pages/RunDetailPage.tsx +++ b/apps/web/src/pages/RunDetailPage.tsx @@ -3,7 +3,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigate, useParams } from "@tanstack/react-router"; import { ChevronDownIcon, RotateCcwIcon, XIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { ArtifactPreview, isPreviewable } from "@/components/artifacts/ArtifactPreview"; +import { ConfirmDialog } from "@/components/ConfirmDialog"; import { FaberAvatar } from "@/components/FaberAvatar"; import { DetailSkeleton } from "@/components/LoadingSkeletons"; import { PageHeader } from "@/components/PageHeader"; @@ -20,7 +22,7 @@ import { RunMetaCard } from "@/features/runs/RunMetaCard"; import { RunTimeline } from "@/features/runs/RunTimeline"; import { useMe } from "../features/me"; import { useRunEvents } from "../features/useRunEvents"; -import { api } from "../lib/api"; +import { ApiError, api } from "../lib/api"; import { formatCost, formatDuration, formatTime, lt } from "../lib/format"; import type { Artifact, Run, RunStep } from "../lib/types"; @@ -100,6 +102,10 @@ export function RunDetailPage() { params: { projectId, runId: result.runId }, }); }, + // re-resolution can reject like submit (grants, credentials, quota…) + onError: (error) => { + toast.error(error instanceof ApiError ? error.message : String(error)); + }, }); if (run.isError) return void run.refetch()} />; @@ -146,16 +152,20 @@ export function RunDetailPage() { } actions={ - isTerminalRunStatus(current.status) ? ( - + // both endpoints require the member role — hide them from viewers + !canRespond ? null : isTerminalRunStatus(current.status) ? ( + + + {t("actions.retry")} + + } + title={t("actions.retryTitle")} + description={t("actions.retryWarning")} + confirmLabel={t("actions.retryConfirm")} + onConfirm={() => retry.mutate()} + /> ) : (