diff --git a/doc/plans/2026-08-21-runner-acpx-provider.md b/doc/plans/2026-08-21-runner-acpx-provider.md index fe7a7fddc87..fd904740a2e 100644 --- a/doc/plans/2026-08-21-runner-acpx-provider.md +++ b/doc/plans/2026-08-21-runner-acpx-provider.md @@ -1,6 +1,6 @@ # ACPX as a First-Class Paperclip Runner Provider -Status: implementation in progress on `PAP-16679-paperclip-runner` and the existing `paperclip-evals` working tree. +Status: provider implementation and paid Pi/Claude qualification complete. Both full rosters have zero integration failures; remaining Claude misses are measured model behavior. ## Goal @@ -18,18 +18,63 @@ ACPX is pinned to `0.13.1`. Arbitrary commands, argv, agents, and model fallback ## Implementation checklist -- [ ] Add strict ACPX configuration and immutable resolved-profile state to TypeScript and Rust. -- [ ] Build a provider-neutral `AcpxRuntimeHost` around `acpx/runtime`. -- [ ] Add a framed Node sidecar and a distinct Rust `AcpxProvider`; never identify it as Codex. -- [ ] Rebase the ACPX runtime patch for spawn PID, stderr, cwd, notifications, client operations, permission requests, and ephemeral spawn environment injection. -- [ ] Isolate ACPX and agent state under the Paperclip instance runtime with `0700` directories and `0600` files. -- [ ] Generalize the authenticated runner tool bridge and implement Pi extension and Claude MCP/permission profiles. -- [ ] Normalize ACP updates to PRP text, tool, workspace, plan, model, usage, runtime-request, completion, and provider-notice events. -- [ ] Support per-turn checkpoint/load and warm reuse; keep permission waits alive and block safely after provider loss. -- [ ] Add adapter/environment configuration, Runner Lab selection/status, and the five `ax-acpx-*` scenarios. -- [ ] Add deterministic Pi-like, Claude-like, Codex-like, malformed, and hanging fake ACP servers. -- [ ] Add provider-aware `paperclip-evals` configs and immutable artifact/report metadata. -- [ ] Run package, protocol, Rust, server, Lab, browser, live smoke, and paid qualification gates. +- [x] Add strict ACPX configuration and immutable resolved-profile state to TypeScript and Rust. +- [x] Build a provider-neutral `AcpxRuntimeHost` around `acpx/runtime`. +- [x] Add a framed Node sidecar and a distinct Rust `AcpxProvider`; never identify it as Codex. +- [x] Rebase the ACPX runtime patch for spawn PID, stderr, cwd, notifications, client operations, permission requests, and ephemeral spawn environment injection. +- [x] Isolate ACPX and agent state under the Paperclip instance runtime with `0700` directories and `0600` files. +- [x] Generalize the authenticated runner tool bridge and implement Pi extension and Claude MCP/permission profiles. +- [x] Normalize ACP updates to PRP text, tool, workspace, plan, model, usage, runtime-request, completion, and provider-notice events. +- [x] Support per-turn checkpoint/load and warm reuse; keep permission waits alive and block safely after provider loss. +- [x] Add adapter/environment configuration, Runner Lab selection/status, and the five `ax-acpx-*` scenarios. +- [x] Add deterministic host, sidecar, bridge, Pi-extension, malformed-frame, process-loss, and permission-wait coverage. Live qualified agents provide the final agent-specific conformance boundary. +- [x] Add provider-aware `paperclip-evals` configs and immutable artifact/report metadata. +- [x] Complete the live paid Pi and Claude roster gates with zero runner, ACPX, ACP-agent, extension/MCP, or bridge failures. + +## Qualification ledger + +### Pi + +- ACPX `0.13.1`, `pi-acp@0.0.33`, and `@earendil-works/pi-coding-agent@0.84.2` resolve by exact package and executable digest. +- The final full 35-case roster ran as `acpx-pi-qualification-03` against the same rebuilt package qualified for Claude, with zero retries and zero runner, ACPX, ACP-agent, extension, or bridge infrastructure failures. +- Behavioral result: 35/35. The earlier `acpx-pi-qualification-02` model miss did not recur; both immutable rosters remain retained. +- Provider-reported total cost was `$0.009493932` and the pricing-ledger estimate was `$0.005323750`. The most expensive case was `$0.000687344`, below the `$0.50` per-case hard cap. +- Immutable attempts retain the exact requested/effective `openrouter/deepseek/deepseek-v4-flash-0731` model with `provider_verified` provenance, biller, token/cache/reasoning usage, ACPX/profile, session, separate process identities, and clean exit metadata without credentials. +- `acpx-pi-metadata-02` independently verified clean exit, nonzero usage, and separate runnerd, sidecar, and ACP-agent process identities in the durable snapshot. +- Runner Lab verified a cold first turn and warm second turn with the same runner PID, sidecar PID, ACPX record, and provider session, plus all five ACPX scenario deep links at desktop and 390 px. + +### Claude + +- Managed Claude authentication is available and the qualified ACP server completes initialization/session negotiation. +- The model issue was an identity-layer mismatch, not a fallback: the pinned Claude ACP server accepts the canonical `claude-sonnet-5`, resolves it through SDK `ModelInfo.resolvedModel`, activates the provider selector `sonnet`, and then reports that selector in ACP status. Paperclip now reapplies the exact canonical model before the first prompt, requires the ACP configuration call to succeed, verifies the pinned reported selector, and only then normalizes `sonnet` back to `claude-sonnet-5` for durable identity, UI, billing, replay, and model history. Any other selector or reroute still fails closed. +- Runner-owned `mcp__paperclip__*` calls are allowed through Claude ACP's local permission layer exactly once so the authenticated PRP bridge remains the authority. Built-in file/process calls still use the durable interactive approval path. +- A pinned `@agentclientprotocol/claude-agent-acp@0.70.0` patch forwards bounded aggregate SDK token/cache usage through ACP `_meta.usage`; raw SDK/ACP payloads remain outside durable state. +- The representative six-case smoke passed mutation, restraint, human confirmation, and workspace-control coverage with zero integration failures; `finish-task` and `block-task` were model-only misses. +- The final full 35-case roster ran as `acpx-claude-qualification-03` with zero retries and zero runner, ACPX, Claude ACP, MCP, or bridge infrastructure failures. Behavioral result: 33/35 (94.3%). In both misses the model wrote prose claiming the requested finish/block action but made no semantic operation call, so Paperclip correctly left the task `in_progress`. +- Provider-reported total cost was `$3.388296350` and the pricing-ledger estimate was `$1.968167100`. The most expensive case was `$0.189559100`, below the `$0.50` per-case hard cap. +- Every final attempt records requested/effective `claude-sonnet-5` with `provider_verified` provenance, nonzero input/output/cache usage, provider cost, ACPX/profile/session identity, separate runnerd/sidecar/agent PIDs, and clean exit metadata. + +### Qualification defect found and fixed + +- A slow Claude workflow exposed that the eval control plane derived its connection-lease TTL from the provider turn timeout. Runnerd durably acknowledged 59 events and three semantic results, but a later shutdown command arrived after the lease expired. +- The connection lease is now independent from the turn deadline and uses at least the production live-transport one-hour boundary. The provider timeout remains a separate classification/cancellation concern. A deterministic regression test locks this separation. +- The affected `workflow-workspace-recovery` case passed immediately after the fix, and the subsequent complete Claude and Pi rosters both finished with zero lease or shutdown failures. + +### Codex conformance control + +- Managed Codex credentials are staged ephemerally into the isolated ACP home and removed on close; the source credential remains untouched and never enters ACPX records. +- Exact model activation for `gpt-5.6-sol` succeeds. +- The live context smoke completed and shut down cleanly, but the ACP agent ignored the offered semantic task-context tool and searched the filesystem. This is recorded as ACP-agent/model behavior, not runner infrastructure, and does not replace the native Codex provider. + +### Verification snapshot + +- Runner TypeScript tests: 637/637. Rust workspace tests: 78/78 library tests plus all binary/provider/process integration tests. Package build and TypeScript/Rust/browser typechecks pass. +- SDK tests: 22/22. Scenario/Runner Lab component tests: 250/250. Server adapter/native-runtime tests: 81/81. +- Browser: Scenario Explorer 43/43 and Runner Lab/issue-thread 174/174, including all five ACPX scenarios, desktop/mobile, keyboard, and accessibility coverage. +- Replay goldens, TypeScript/Rust conformance parity, TypeScript/Rust replay parity, ACPX patch packaging (9/9), package boundaries, and the full offline clean-consumer pack/install dependency closure pass. +- Paperclip Evals Python tests: 35/35. Both provider rosters validate 35/35 and their final paid attempts ran without automatic retries. +- Final Pi/Claude artifact scans found no OpenRouter credential value, Anthropic/OpenRouter credential-shaped value, or Paperclip key assignment. No qualification runnerd, ACPX sidecar, Pi, or Claude ACP process remained afterward. +- The tracked-import gate passes against a prospective index containing the new Claude environment module. The real working index remains untouched until the surrounding working-tree changes are committed. ## Security invariants diff --git a/package.json b/package.json index 8910842e1aa..5f82c62048f 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ "patchedDependencies": { "embedded-postgres@18.1.0-beta.16": "patches/embedded-postgres@18.1.0-beta.16.patch", "acpx@0.13.1": "patches/acpx@0.13.1.patch", - "pi-acp@0.0.33": "patches/pi-acp@0.0.33.patch" + "pi-acp@0.0.33": "patches/pi-acp@0.0.33.patch", + "@agentclientprotocol/claude-agent-acp@0.70.0": "patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch" }, "overrides": { "rollup": ">=4.59.0", diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index eba04d4f0ca..b0df5c6e1b3 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -566,7 +566,7 @@ export interface TranscriptRunArtifact { export type TranscriptEntry = | { kind: "assistant"; ts: string; text: string; delta?: boolean; channel?: "progress" | "final" | "unknown" } - | { kind: "thinking"; ts: string; text: string; delta?: boolean; channel?: "summary" | "detail" | "unknown" } + | { kind: "thinking"; ts: string; text: string; delta?: boolean; lifecycle?: "started" | "completed"; channel?: "summary" | "detail" | "unknown" } | { kind: "user"; ts: string; text: string } | { kind: "tool_call"; ts: string; name: string; input: unknown; toolUseId?: string; invocationId?: string; actionRequestId?: string } | { kind: "tool_result"; ts: string; toolUseId: string; toolName?: string; content: string; isError: boolean; delta?: boolean } diff --git a/packages/db/src/migrations/0229_pink_longshot.sql b/packages/db/src/migrations/0229_pink_longshot.sql new file mode 100644 index 00000000000..3bdca0f835b --- /dev/null +++ b/packages/db/src/migrations/0229_pink_longshot.sql @@ -0,0 +1,23 @@ +CREATE TABLE "provider_trace_records" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "run_id" uuid NOT NULL, + "status" text DEFAULT 'capturing' NOT NULL, + "provider" text NOT NULL, + "trace_ref" text NOT NULL, + "frame_count" integer DEFAULT 0 NOT NULL, + "byte_count" bigint DEFAULT 0 NOT NULL, + "digest" text, + "reason" text, + "requested_by" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "deleted_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "provider_trace_records" ADD CONSTRAINT "provider_trace_records_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "provider_trace_records" ADD CONSTRAINT "provider_trace_records_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "provider_trace_records_run_unique" ON "provider_trace_records" USING btree ("run_id");--> statement-breakpoint +CREATE INDEX "provider_trace_records_expiry_idx" ON "provider_trace_records" USING btree ("status","expires_at");--> statement-breakpoint +CREATE INDEX "provider_trace_records_company_created_idx" ON "provider_trace_records" USING btree ("company_id","created_at"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0229_snapshot.json b/packages/db/src/migrations/meta/0229_snapshot.json new file mode 100644 index 00000000000..fc3d3f6fa19 --- /dev/null +++ b/packages/db/src/migrations/meta/0229_snapshot.json @@ -0,0 +1,41613 @@ +{ + "id": "e90810ea-b7e8-4c1e-8615-3d8926b22bae", + "prevId": "a774e1ce-7304-4f42-8cd2-d8dfe4ddf45b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_company_agent_created_idx": { + "name": "activity_log_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_company_responsible_user_created_idx": { + "name": "activity_log_company_responsible_user_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.adapter_auth_sessions": { + "name": "adapter_auth_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_session_id": { + "name": "public_session_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starting'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "promotion_expires_at": { + "name": "promotion_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bound_at": { + "name": "bound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "adapter_auth_sessions_company_status_idx": { + "name": "adapter_auth_sessions_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_company_owner_adapter_active_uq": { + "name": "adapter_auth_sessions_company_owner_adapter_active_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"adapter_auth_sessions\".\"status\" IN ('starting', 'waiting_for_user', 'promoting', 'awaiting_code', 'submitting')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_public_session_id_uq": { + "name": "adapter_auth_sessions_public_session_id_uq", + "columns": [ + { + "expression": "public_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_environment_idx": { + "name": "adapter_auth_sessions_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_expires_idx": { + "name": "adapter_auth_sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_provider_lease_idx": { + "name": "adapter_auth_sessions_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "adapter_auth_sessions_company_id_companies_id_fk": { + "name": "adapter_auth_sessions_company_id_companies_id_fk", + "tableFrom": "adapter_auth_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "adapter_auth_sessions_environment_id_environments_id_fk": { + "name": "adapter_auth_sessions_environment_id_environments_id_fk", + "tableFrom": "adapter_auth_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_config": { + "name": "scope_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_memberships": { + "name": "agent_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'joined'" + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_memberships_company_user_idx": { + "name": "agent_memberships_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_company_user_starred_idx": { + "name": "agent_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_agent_idx": { + "name": "agent_memberships_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_company_user_agent_uq": { + "name": "agent_memberships_company_user_agent_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memberships_company_id_companies_id_fk": { + "name": "agent_memberships_company_id_companies_id_fk", + "tableFrom": "agent_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memberships_agent_id_agents_id_fk": { + "name": "agent_memberships_agent_id_agents_id_fk", + "tableFrom": "agent_memberships", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_review_path_recovery_idempotency_uq": { + "name": "agent_wakeup_requests_review_path_recovery_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'issue_review_path_lost:%' AND \"agent_wakeup_requests\".\"status\" <> 'skipped'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_payload_issue_idx": { + "name": "agent_wakeup_requests_company_payload_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"payload\" ->> 'issueId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_reason": { + "name": "error_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_default_environment_idx": { + "name": "agents_company_default_environment_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "default_environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_default_environment_id_environments_id_fk": { + "name": "agents_default_environment_id_environments_id_fk", + "tableFrom": "agents", + "tableTo": "environments", + "columnsFrom": [ + "default_environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_api_keys": { + "name": "board_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_api_keys_key_hash_idx": { + "name": "board_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.built_in_managed_resources": { + "name": "built_in_managed_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bundle_key": { + "name": "bundle_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stock_version": { + "name": "stock_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stock_hash": { + "name": "stock_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "defaults_json": { + "name": "defaults_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "built_in_managed_resources_company_idx": { + "name": "built_in_managed_resources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "built_in_managed_resources_resource_idx": { + "name": "built_in_managed_resources_resource_idx", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "built_in_managed_resources_company_bundle_resource_uq": { + "name": "built_in_managed_resources_company_bundle_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bundle_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "built_in_managed_resources_company_id_companies_id_fk": { + "name": "built_in_managed_resources_company_id_companies_id_fk", + "tableFrom": "built_in_managed_resources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_attachments": { + "name": "case_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_attachments_company_case_idx": { + "name": "case_attachments_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_attachments_asset_uq": { + "name": "case_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_attachments_company_id_companies_id_fk": { + "name": "case_attachments_company_id_companies_id_fk", + "tableFrom": "case_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_attachments_case_id_cases_id_fk": { + "name": "case_attachments_case_id_cases_id_fk", + "tableFrom": "case_attachments", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_attachments_asset_id_assets_id_fk": { + "name": "case_attachments_asset_id_assets_id_fk", + "tableFrom": "case_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_documents": { + "name": "case_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_documents_company_case_key_uq": { + "name": "case_documents_company_case_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_documents_document_uq": { + "name": "case_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_documents_company_case_updated_idx": { + "name": "case_documents_company_case_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_documents_company_id_companies_id_fk": { + "name": "case_documents_company_id_companies_id_fk", + "tableFrom": "case_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_documents_case_id_cases_id_fk": { + "name": "case_documents_case_id_cases_id_fk", + "tableFrom": "case_documents", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_documents_document_id_documents_id_fk": { + "name": "case_documents_document_id_documents_id_fk", + "tableFrom": "case_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_events": { + "name": "case_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_events_case_created_idx": { + "name": "case_events_case_created_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_events_company_case_idx": { + "name": "case_events_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_events_company_id_companies_id_fk": { + "name": "case_events_company_id_companies_id_fk", + "tableFrom": "case_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_events_case_id_cases_id_fk": { + "name": "case_events_case_id_cases_id_fk", + "tableFrom": "case_events", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_events_actor_agent_id_agents_id_fk": { + "name": "case_events_actor_agent_id_agents_id_fk", + "tableFrom": "case_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "case_events_kind_check": { + "name": "case_events_kind_check", + "value": "\"case_events\".\"kind\" in (\n 'created',\n 'updated',\n 'fields_changed',\n 'status_changed',\n 'issue_linked',\n 'issue_unlinked',\n 'document_revised',\n 'child_linked',\n 'attachment_added',\n 'label_added',\n 'label_removed'\n )" + }, + "case_events_actor_type_check": { + "name": "case_events_actor_type_check", + "value": "\"case_events\".\"actor_type\" in ('user', 'agent', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.case_issue_links": { + "name": "case_issue_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_issue_links_case_issue_uq": { + "name": "case_issue_links_case_issue_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_issue_links_company_case_idx": { + "name": "case_issue_links_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_issue_links_issue_idx": { + "name": "case_issue_links_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_issue_links_company_id_companies_id_fk": { + "name": "case_issue_links_company_id_companies_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_issue_links_case_id_cases_id_fk": { + "name": "case_issue_links_case_id_cases_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_issue_links_issue_id_issues_id_fk": { + "name": "case_issue_links_issue_id_issues_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "case_issue_links_role_check": { + "name": "case_issue_links_role_check", + "value": "\"case_issue_links\".\"role\" in ('origin', 'work', 'reference')" + } + }, + "isRLSEnabled": false + }, + "public.case_labels": { + "name": "case_labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_labels_case_label_uq": { + "name": "case_labels_case_label_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_labels_company_case_idx": { + "name": "case_labels_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_labels_label_idx": { + "name": "case_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_labels_company_id_companies_id_fk": { + "name": "case_labels_company_id_companies_id_fk", + "tableFrom": "case_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_labels_case_id_cases_id_fk": { + "name": "case_labels_case_id_cases_id_fk", + "tableFrom": "case_labels", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_labels_label_id_labels_id_fk": { + "name": "case_labels_label_id_labels_id_fk", + "tableFrom": "case_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cases": { + "name": "cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_number": { + "name": "case_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "case_type": { + "name": "case_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "parent_case_id": { + "name": "parent_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cases_company_case_number_uq": { + "name": "cases_company_case_number_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_identifier_uq": { + "name": "cases_identifier_uq", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_type_key_uq": { + "name": "cases_company_type_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_status_idx": { + "name": "cases_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_type_idx": { + "name": "cases_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_project_idx": { + "name": "cases_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_parent_idx": { + "name": "cases_parent_idx", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_title_search_idx": { + "name": "cases_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "cases_identifier_search_idx": { + "name": "cases_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "cases_summary_search_idx": { + "name": "cases_summary_search_idx", + "columns": [ + { + "expression": "summary", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "cases_company_id_companies_id_fk": { + "name": "cases_company_id_companies_id_fk", + "tableFrom": "cases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cases_project_id_projects_id_fk": { + "name": "cases_project_id_projects_id_fk", + "tableFrom": "cases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cases_parent_case_id_cases_id_fk": { + "name": "cases_parent_case_id_cases_id_fk", + "tableFrom": "cases", + "tableTo": "cases", + "columnsFrom": [ + "parent_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cases_created_by_agent_id_agents_id_fk": { + "name": "cases_created_by_agent_id_agents_id_fk", + "tableFrom": "cases", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cases_status_check": { + "name": "cases_status_check", + "value": "\"cases\".\"status\" in ('draft', 'in_progress', 'in_review', 'approved', 'done', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.cli_auth_challenges": { + "name": "cli_auth_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_access": { + "name": "requested_access", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'board'" + }, + "requested_company_id": { + "name": "requested_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_key_hash": { + "name": "pending_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_key_name": { + "name": "pending_key_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_api_key_id": { + "name": "board_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "cli_auth_challenges_secret_hash_idx": { + "name": "cli_auth_challenges_secret_hash_idx", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_approved_by_idx": { + "name": "cli_auth_challenges_approved_by_idx", + "columns": [ + { + "expression": "approved_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_requested_company_idx": { + "name": "cli_auth_challenges_requested_company_idx", + "columns": [ + { + "expression": "requested_company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "companies", + "columnsFrom": [ + "requested_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_approved_by_user_id_user_id_fk": { + "name": "cli_auth_challenges_approved_by_user_id_user_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "user", + "columnsFrom": [ + "approved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk": { + "name": "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "board_api_keys", + "columnsFrom": [ + "board_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attachment_max_bytes": { + "name": "attachment_max_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10485760 + }, + "default_responsible_user_id": { + "name": "default_responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "interaction_resolver_governance": { + "name": "interaction_resolver_governance", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "feedback_data_sharing_enabled": { + "name": "feedback_data_sharing_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_consent_at": { + "name": "feedback_data_sharing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_consent_by_user_id": { + "name": "feedback_data_sharing_consent_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_terms_version": { + "name": "feedback_data_sharing_terms_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_onboarding_seeds": { + "name": "company_onboarding_seeds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mission": { + "name": "mission", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_role": { + "name": "agent_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_task_title": { + "name": "first_task_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_task_details": { + "name": "first_task_details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_onboarding_seeds_company_uq": { + "name": "company_onboarding_seeds_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_onboarding_seeds_company_id_companies_id_fk": { + "name": "company_onboarding_seeds_company_id_companies_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_onboarding_seeds_goal_id_goals_id_fk": { + "name": "company_onboarding_seeds_goal_id_goals_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_onboarding_seeds_agent_id_agents_id_fk": { + "name": "company_onboarding_seeds_agent_id_agents_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_onboarding_seeds_issue_id_issues_id_fk": { + "name": "company_onboarding_seeds_issue_id_issues_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_bindings": { + "name": "company_secret_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_selector": { + "name": "version_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'latest'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projection_class": { + "name": "projection_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unclassified'" + }, + "projection_allowlist_key": { + "name": "projection_allowlist_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secret_bindings_company_idx": { + "name": "company_secret_bindings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_secret_idx": { + "name": "company_secret_bindings_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_target_idx": { + "name": "company_secret_bindings_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_target_path_uq": { + "name": "company_secret_bindings_target_path_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_bindings_company_id_companies_id_fk": { + "name": "company_secret_bindings_company_id_companies_id_fk", + "tableFrom": "company_secret_bindings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secret_bindings_secret_id_company_secrets_id_fk": { + "name": "company_secret_bindings_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_bindings", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_proposals": { + "name": "company_secret_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "proposed_name": { + "name": "proposed_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed_key": { + "name": "proposed_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed_description": { + "name": "proposed_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "justification": { + "name": "justification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_ciphertext": { + "name": "value_ciphertext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "value_fingerprint_sha256": { + "name": "value_fingerprint_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value_length": { + "name": "value_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "secret_proposal_id": { + "name": "secret_proposal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projection_class": { + "name": "projection_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unclassified'" + }, + "binding_target_policy_snapshot": { + "name": "binding_target_policy_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposer_ancestor_ids_snapshot": { + "name": "proposer_ancestor_ids_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_ancestor_ids_snapshot": { + "name": "target_ancestor_ids_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "proposed_by_agent_id": { + "name": "proposed_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "interaction_id": { + "name": "interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_secret_id": { + "name": "created_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applied_binding_config_path": { + "name": "applied_binding_config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ciphertext_scrubbed_at": { + "name": "ciphertext_scrubbed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "company_secret_proposals_company_status_idx": { + "name": "company_secret_proposals_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_proposals_proposer_status_idx": { + "name": "company_secret_proposals_proposer_status_idx", + "columns": [ + { + "expression": "proposed_by_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_proposals_expiry_idx": { + "name": "company_secret_proposals_expiry_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_proposals_secret_proposal_idx": { + "name": "company_secret_proposals_secret_proposal_idx", + "columns": [ + { + "expression": "secret_proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_proposals_interaction_idx": { + "name": "company_secret_proposals_interaction_idx", + "columns": [ + { + "expression": "interaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_proposals_company_id_companies_id_fk": { + "name": "company_secret_proposals_company_id_companies_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secret_proposals_secret_id_company_secrets_id_fk": { + "name": "company_secret_proposals_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secret_proposals_secret_proposal_id_company_secret_proposals_id_fk": { + "name": "company_secret_proposals_secret_proposal_id_company_secret_proposals_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secret_proposals", + "columnsFrom": [ + "secret_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_target_id_agents_id_fk": { + "name": "company_secret_proposals_target_id_agents_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "agents", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_proposed_by_agent_id_agents_id_fk": { + "name": "company_secret_proposals_proposed_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "agents", + "columnsFrom": [ + "proposed_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_origin_issue_id_issues_id_fk": { + "name": "company_secret_proposals_origin_issue_id_issues_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secret_proposals_origin_run_id_heartbeat_runs_id_fk": { + "name": "company_secret_proposals_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_interaction_id_issue_thread_interactions_id_fk": { + "name": "company_secret_proposals_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "issue_thread_interactions", + "columnsFrom": [ + "interaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secret_proposals_created_secret_id_company_secrets_id_fk": { + "name": "company_secret_proposals_created_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secrets", + "columnsFrom": [ + "created_secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "company_secret_proposals_kind_check": { + "name": "company_secret_proposals_kind_check", + "value": "\"company_secret_proposals\".\"kind\" in ('secret', 'binding')" + }, + "company_secret_proposals_status_check": { + "name": "company_secret_proposals_status_check", + "value": "\"company_secret_proposals\".\"status\" in ('pending', 'approved', 'rejected', 'withdrawn', 'expired')" + }, + "company_secret_proposals_projection_check": { + "name": "company_secret_proposals_projection_check", + "value": "\"company_secret_proposals\".\"projection_class\" = 'unclassified'" + }, + "company_secret_proposals_shape_check": { + "name": "company_secret_proposals_shape_check", + "value": "(\n \"company_secret_proposals\".\"kind\" = 'secret'\n and \"company_secret_proposals\".\"proposed_name\" is not null\n and \"company_secret_proposals\".\"proposed_key\" is not null\n and \"company_secret_proposals\".\"secret_id\" is null\n and \"company_secret_proposals\".\"secret_proposal_id\" is null\n and \"company_secret_proposals\".\"target_type\" is null\n and \"company_secret_proposals\".\"target_id\" is null\n and \"company_secret_proposals\".\"config_path\" is null\n ) or (\n \"company_secret_proposals\".\"kind\" = 'binding'\n and ((\"company_secret_proposals\".\"secret_id\" is not null)::int + (\"company_secret_proposals\".\"secret_proposal_id\" is not null)::int) = 1\n and \"company_secret_proposals\".\"target_type\" = 'agent'\n and \"company_secret_proposals\".\"target_id\" is not null\n and \"company_secret_proposals\".\"config_path\" is not null\n )" + } + }, + "isRLSEnabled": false + }, + "public.company_secret_provider_configs": { + "name": "company_secret_provider_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_checked_at": { + "name": "health_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_details": { + "name": "health_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secret_provider_configs_company_idx": { + "name": "company_secret_provider_configs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_provider_configs_company_provider_idx": { + "name": "company_secret_provider_configs_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_provider_configs_default_uq": { + "name": "company_secret_provider_configs_default_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secret_provider_configs\".\"is_default\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_provider_configs_company_id_companies_id_fk": { + "name": "company_secret_provider_configs_company_id_companies_id_fk", + "tableFrom": "company_secret_provider_configs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_provider_configs_created_by_agent_id_agents_id_fk": { + "name": "company_secret_provider_configs_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_provider_configs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_version_ref": { + "name": "provider_version_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'current'" + }, + "fingerprint_sha256": { + "name": "fingerprint_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotation_job_id": { + "name": "rotation_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_fingerprint_idx": { + "name": "company_secret_versions_fingerprint_idx", + "columns": [ + { + "expression": "fingerprint_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "managed_mode": { + "name": "managed_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip_managed'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config_id": { + "name": "provider_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_at": { + "name": "last_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_scope_idx": { + "name": "company_secrets_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_owner_idx": { + "name": "company_secrets_company_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_user_definition_owner_idx": { + "name": "company_secrets_user_definition_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_provider_config_idx": { + "name": "company_secrets_provider_config_idx", + "columns": [ + { + "expression": "provider_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'company' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_key_uq": { + "name": "company_secrets_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'company' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_user_definition_owner_uq": { + "name": "company_secrets_user_definition_owner_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'user' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "company_secrets", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secrets_provider_config_id_company_secret_provider_configs_id_fk": { + "name": "company_secrets_provider_config_id_company_secret_provider_configs_id_fk", + "tableFrom": "company_secrets", + "tableTo": "company_secret_provider_configs", + "columnsFrom": [ + "provider_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "company_secrets_scope_shape_check": { + "name": "company_secrets_scope_shape_check", + "value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n )" + } + }, + "isRLSEnabled": false + }, + "public.company_skill_policies": { + "name": "company_skill_policies", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "default_effect": { + "name": "default_effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rules": { + "name": "rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "company_skill_policies_company_id_companies_id_fk": { + "name": "company_skill_policies_company_id_companies_id_fk", + "tableFrom": "company_skill_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_comments": { + "name": "company_skill_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skill_comments_company_skill_created_idx": { + "name": "company_skill_comments_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_comments_parent_idx": { + "name": "company_skill_comments_parent_idx", + "columns": [ + { + "expression": "parent_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_comments_company_id_companies_id_fk": { + "name": "company_skill_comments_company_id_companies_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_comments_company_skill_id_company_skills_id_fk": { + "name": "company_skill_comments_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_comments_parent_comment_id_company_skill_comments_id_fk": { + "name": "company_skill_comments_parent_comment_id_company_skill_comments_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "company_skill_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_comments_author_agent_id_agents_id_fk": { + "name": "company_skill_comments_author_agent_id_agents_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_stars": { + "name": "company_skill_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skill_stars_skill_agent_idx": { + "name": "company_skill_stars_skill_agent_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_stars_skill_user_idx": { + "name": "company_skill_stars_skill_user_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_stars_company_skill_created_idx": { + "name": "company_skill_stars_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_stars_company_id_companies_id_fk": { + "name": "company_skill_stars_company_id_companies_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_stars_company_skill_id_company_skills_id_fk": { + "name": "company_skill_stars_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_stars_agent_id_agents_id_fk": { + "name": "company_skill_stars_agent_id_agents_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_inputs": { + "name": "company_skill_test_inputs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skill_test_inputs_company_skill_name_idx": { + "name": "company_skill_test_inputs_company_skill_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_inputs_company_skill_active_idx": { + "name": "company_skill_test_inputs_company_skill_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_inputs_company_id_companies_id_fk": { + "name": "company_skill_test_inputs_company_id_companies_id_fk", + "tableFrom": "company_skill_test_inputs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_inputs_skill_id_company_skills_id_fk": { + "name": "company_skill_test_inputs_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_test_inputs", + "tableTo": "company_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_run_templates": { + "name": "company_skill_test_run_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skill_test_run_templates_company_active_idx": { + "name": "company_skill_test_run_templates_company_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_run_templates_company_id_companies_id_fk": { + "name": "company_skill_test_run_templates_company_id_companies_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_run_templates_created_by_agent_id_agents_id_fk": { + "name": "company_skill_test_run_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_test_run_templates_updated_by_agent_id_agents_id_fk": { + "name": "company_skill_test_run_templates_updated_by_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_runs": { + "name": "company_skill_test_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "input_id": { + "name": "input_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "input_snapshot": { + "name": "input_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skill_version_id": { + "name": "skill_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_config_snapshot": { + "name": "agent_config_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_name": { + "name": "template_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_body": { + "name": "template_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_template_body": { + "name": "rendered_template_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness_issue_description": { + "name": "harness_issue_description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "output_document_key": { + "name": "output_document_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'output'" + }, + "output_snapshot": { + "name": "output_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "harness_issue_expires_at": { + "name": "harness_issue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "harness_issue_deleted_at": { + "name": "harness_issue_deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skill_test_runs_company_skill_created_idx": { + "name": "company_skill_test_runs_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_issue_idx": { + "name": "company_skill_test_runs_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_input_created_idx": { + "name": "company_skill_test_runs_company_input_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "input_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_status_idx": { + "name": "company_skill_test_runs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_harness_expires_idx": { + "name": "company_skill_test_runs_company_harness_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "harness_issue_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_runs_company_id_companies_id_fk": { + "name": "company_skill_test_runs_company_id_companies_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_runs_skill_id_company_skills_id_fk": { + "name": "company_skill_test_runs_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_runs_input_id_company_skill_test_inputs_id_fk": { + "name": "company_skill_test_runs_input_id_company_skill_test_inputs_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skill_test_inputs", + "columnsFrom": [ + "input_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_test_runs_skill_version_id_company_skill_versions_id_fk": { + "name": "company_skill_test_runs_skill_version_id_company_skill_versions_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skill_versions", + "columnsFrom": [ + "skill_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "company_skill_test_runs_agent_id_agents_id_fk": { + "name": "company_skill_test_runs_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "company_skill_test_runs_issue_id_issues_id_fk": { + "name": "company_skill_test_runs_issue_id_issues_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_versions": { + "name": "company_skill_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_name": { + "name": "release_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skill_versions_skill_revision_idx": { + "name": "company_skill_versions_skill_revision_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_versions_skill_release_idx": { + "name": "company_skill_versions_skill_release_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_skill_versions\".\"release_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_versions_company_skill_created_idx": { + "name": "company_skill_versions_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_versions_company_id_companies_id_fk": { + "name": "company_skill_versions_company_id_companies_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_versions_company_skill_id_company_skills_id_fk": { + "name": "company_skill_versions_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_versions_author_agent_id_agents_id_fk": { + "name": "company_skill_versions_author_agent_id_agents_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skills": { + "name": "company_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "source_locator": { + "name": "source_locator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_level": { + "name": "trust_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown_only'" + }, + "compatibility": { + "name": "compatibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compatible'" + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "homepage_url": { + "name": "homepage_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sharing_scope": { + "name": "sharing_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "public_share_token": { + "name": "public_share_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "forked_from_company_id": { + "name": "forked_from_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "star_count": { + "name": "star_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "install_count": { + "name": "install_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fork_count": { + "name": "fork_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "current_version_id": { + "name": "current_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skills_company_key_idx": { + "name": "company_skills_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_name_idx": { + "name": "company_skills_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_folder_idx": { + "name": "company_skills_company_folder_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_categories_idx": { + "name": "company_skills_company_categories_idx", + "columns": [ + { + "expression": "categories", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "company_skills_company_sharing_scope_idx": { + "name": "company_skills_company_sharing_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sharing_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_current_version_idx": { + "name": "company_skills_company_current_version_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_forked_from_idx": { + "name": "company_skills_company_forked_from_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "forked_from_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_skills_folder_id_folders_id_fk": { + "name": "company_skills_folder_id_folders_id_fk", + "tableFrom": "company_skills", + "tableTo": "folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_forked_from_skill_id_company_skills_id_fk": { + "name": "company_skills_forked_from_skill_id_company_skills_id_fk", + "tableFrom": "company_skills", + "tableTo": "company_skills", + "columnsFrom": [ + "forked_from_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_forked_from_company_id_companies_id_fk": { + "name": "company_skills_forked_from_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "forked_from_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_current_version_id_company_skill_versions_id_fk": { + "name": "company_skills_current_version_id_company_skill_versions_id_fk", + "tableFrom": "company_skills", + "tableTo": "company_skill_versions", + "columnsFrom": [ + "current_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_transfer_runs": { + "name": "company_transfer_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "actor_key": { + "name": "actor_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_ref": { + "name": "container_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest_sha256": { + "name": "manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "blob_count": { + "name": "blob_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_parts": { + "name": "completed_parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "error": { + "name": "error", + "type": "text", + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_transfer_runs_company_idx": { + "name": "company_transfer_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_transfer_runs_idempotency_direction_idx": { + "name": "company_transfer_runs_idempotency_direction_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_transfer_runs_actor_status_idx": { + "name": "company_transfer_runs_actor_status_idx", + "columns": [ + { + "expression": "actor_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_transfer_runs_company_id_companies_id_fk": { + "name": "company_transfer_runs_company_id_companies_id_fk", + "tableFrom": "company_transfer_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_user_sidebar_preferences": { + "name": "company_user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_order": { + "name": "project_order", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_user_sidebar_preferences_company_idx": { + "name": "company_user_sidebar_preferences_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_user_idx": { + "name": "company_user_sidebar_preferences_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_company_user_uq": { + "name": "company_user_sidebar_preferences_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_user_sidebar_preferences_company_id_companies_id_fk": { + "name": "company_user_sidebar_preferences_company_id_companies_id_fk", + "tableFrom": "company_user_sidebar_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.completion_contracts": { + "name": "completion_contracts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_version": { + "name": "policy_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completion_authority": { + "name": "completion_authority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incomplete_criteria_policy": { + "name": "incomplete_criteria_policy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contract_json": { + "name": "contract_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "canonical_sha256": { + "name": "canonical_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "supersedes_contract_id": { + "name": "supersedes_contract_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "completion_contracts_issue_revision_uq": { + "name": "completion_contracts_issue_revision_uq", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "completion_contracts_issue_hash_uq": { + "name": "completion_contracts_issue_hash_uq", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "completion_contracts_company_id_companies_id_fk": { + "name": "completion_contracts_company_id_companies_id_fk", + "tableFrom": "completion_contracts", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "completion_contracts_issue_id_issues_id_fk": { + "name": "completion_contracts_issue_id_issues_id_fk", + "tableFrom": "completion_contracts", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "cost_status": { + "name": "cost_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reported'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_archive_notification_outbox": { + "name": "decision_archive_notification_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archive_version": { + "name": "archive_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_archive_notification_outbox_uq": { + "name": "decision_archive_notification_outbox_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archive_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_archive_notification_outbox_pending_idx": { + "name": "decision_archive_notification_outbox_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_archive_notification_outbox_company_id_companies_id_fk": { + "name": "decision_archive_notification_outbox_company_id_companies_id_fk", + "tableFrom": "decision_archive_notification_outbox", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_archive_notification_outbox_origin_agent_id_agents_id_fk": { + "name": "decision_archive_notification_outbox_origin_agent_id_agents_id_fk", + "tableFrom": "decision_archive_notification_outbox", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_archive_notification_outbox_status_check": { + "name": "decision_archive_notification_outbox_status_check", + "value": "\"decision_archive_notification_outbox\".\"status\" IN ('pending', 'delivering', 'delivered')" + } + }, + "isRLSEnabled": false + }, + "public.decision_queue_items": { + "name": "decision_queue_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_type": { + "name": "added_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_agent_id": { + "name": "added_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by_run_id": { + "name": "added_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "added_by_agent_api_key_id": { + "name": "added_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_queue_items_queue_source_uq": { + "name": "decision_queue_items_queue_source_uq", + "columns": [ + { + "expression": "queue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_queue_items_company_source_idx": { + "name": "decision_queue_items_company_source_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_queue_items_company_id_companies_id_fk": { + "name": "decision_queue_items_company_id_companies_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_agent_id_agents_id_fk": { + "name": "decision_queue_items_added_by_agent_id_agents_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "agents", + "columnsFrom": [ + "added_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_queue_items_added_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "added_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_queue_items_added_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "added_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_queue_company_fk": { + "name": "decision_queue_items_queue_company_fk", + "tableFrom": "decision_queue_items", + "tableTo": "decision_queues", + "columnsFrom": [ + "queue_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_queue_items_actor_check": { + "name": "decision_queue_items_actor_check", + "value": "(\n (\"decision_queue_items\".\"added_by_type\" = 'agent' AND \"decision_queue_items\".\"added_by_agent_id\" IS NOT NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NULL)\n OR (\"decision_queue_items\".\"added_by_type\" = 'user' AND \"decision_queue_items\".\"added_by_agent_id\" IS NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NOT NULL)\n OR (\"decision_queue_items\".\"added_by_type\" = 'system' AND \"decision_queue_items\".\"added_by_agent_id\" IS NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_queues": { + "name": "decision_queues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_type": { + "name": "created_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_api_key_id": { + "name": "created_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retention_days": { + "name": "retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seed_rules": { + "name": "seed_rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "seed_rules_enabled": { + "name": "seed_rules_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_queues_company_key_uq": { + "name": "decision_queues_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_queues_company_updated_idx": { + "name": "decision_queues_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_queues_company_id_companies_id_fk": { + "name": "decision_queues_company_id_companies_id_fk", + "tableFrom": "decision_queues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_queues_created_by_agent_id_agents_id_fk": { + "name": "decision_queues_created_by_agent_id_agents_id_fk", + "tableFrom": "decision_queues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queues_created_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_queues_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_queues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queues_created_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_queues_created_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_queues", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "created_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "decision_queues_id_company_uq": { + "name": "decision_queues_id_company_uq", + "nullsNotDistinct": false, + "columns": [ + "id", + "company_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "decision_queues_creator_check": { + "name": "decision_queues_creator_check", + "value": "(\n (\"decision_queues\".\"created_by_type\" = 'agent' AND \"decision_queues\".\"created_by_agent_id\" IS NOT NULL AND \"decision_queues\".\"created_by_user_id\" IS NULL)\n OR (\"decision_queues\".\"created_by_type\" = 'user' AND \"decision_queues\".\"created_by_agent_id\" IS NULL AND \"decision_queues\".\"created_by_user_id\" IS NOT NULL)\n OR (\"decision_queues\".\"created_by_type\" = 'system' AND \"decision_queues\".\"created_by_agent_id\" IS NULL AND \"decision_queues\".\"created_by_user_id\" IS NULL)\n )" + }, + "decision_queues_retention_days_check": { + "name": "decision_queues_retention_days_check", + "value": "\"decision_queues\".\"retention_days\" IS NULL OR (\"decision_queues\".\"retention_days\" >= 1 AND \"decision_queues\".\"retention_days\" <= 3650)" + } + }, + "isRLSEnabled": false + }, + "public.decision_retention": { + "name": "decision_retention", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_activity_at": { + "name": "source_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_reason": { + "name": "archived_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_type": { + "name": "archived_by_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_agent_id": { + "name": "archived_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_by_user_id": { + "name": "archived_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_run_id": { + "name": "archived_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "archive_version": { + "name": "archive_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_retention_company_source_uq": { + "name": "decision_retention_company_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_retention_company_archived_idx": { + "name": "decision_retention_company_archived_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_retention_company_id_companies_id_fk": { + "name": "decision_retention_company_id_companies_id_fk", + "tableFrom": "decision_retention", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_retention_archived_by_agent_id_agents_id_fk": { + "name": "decision_retention_archived_by_agent_id_agents_id_fk", + "tableFrom": "decision_retention", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_retention_archived_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_retention_archived_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_retention", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "archived_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_retention_archive_actor_check": { + "name": "decision_retention_archive_actor_check", + "value": "(\n (\"decision_retention\".\"archived_at\" IS NULL AND \"decision_retention\".\"archived_by_type\" IS NULL AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'system' AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'agent' AND \"decision_retention\".\"archived_by_agent_id\" IS NOT NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'user' AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_triage": { + "name": "decision_triage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decide_by": { + "name": "decide_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decide_by_date": { + "name": "decide_by_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "set_by_type": { + "name": "set_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "set_by_agent_id": { + "name": "set_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "set_by_user_id": { + "name": "set_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "set_by_run_id": { + "name": "set_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "set_by_agent_api_key_id": { + "name": "set_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_triage_company_source_uq": { + "name": "decision_triage_company_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_triage_company_decide_by_idx": { + "name": "decision_triage_company_decide_by_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decide_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_triage_company_id_companies_id_fk": { + "name": "decision_triage_company_id_companies_id_fk", + "tableFrom": "decision_triage", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_triage_set_by_agent_id_agents_id_fk": { + "name": "decision_triage_set_by_agent_id_agents_id_fk", + "tableFrom": "decision_triage", + "tableTo": "agents", + "columnsFrom": [ + "set_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_set_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_triage_set_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_triage", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "set_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_set_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_triage_set_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_triage", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "set_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_triage_actor_check": { + "name": "decision_triage_actor_check", + "value": "(\n (\"decision_triage\".\"set_by_type\" = 'agent' AND \"decision_triage\".\"set_by_agent_id\" IS NOT NULL AND \"decision_triage\".\"set_by_user_id\" IS NULL)\n OR (\"decision_triage\".\"set_by_type\" = 'user' AND \"decision_triage\".\"set_by_agent_id\" IS NULL AND \"decision_triage\".\"set_by_user_id\" IS NOT NULL)\n )" + }, + "decision_triage_decide_by_check": { + "name": "decision_triage_decide_by_check", + "value": "(\n (\"decision_triage\".\"decide_by\" IS NULL AND \"decision_triage\".\"decide_by_date\" IS NULL)\n OR (\"decision_triage\".\"decide_by\" IN ('today', 'this_week', 'whenever') AND \"decision_triage\".\"decide_by_date\" IS NULL)\n OR (\"decision_triage\".\"decide_by\" = 'date' AND \"decision_triage\".\"decide_by_date\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_triage_events": { + "name": "decision_triage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_run_id": { + "name": "actor_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_api_key_id": { + "name": "agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "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": { + "decision_triage_events_company_source_created_idx": { + "name": "decision_triage_events_company_source_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_triage_events_queue_created_idx": { + "name": "decision_triage_events_queue_created_idx", + "columns": [ + { + "expression": "queue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_triage_events_company_id_companies_id_fk": { + "name": "decision_triage_events_company_id_companies_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_triage_events_queue_id_decision_queues_id_fk": { + "name": "decision_triage_events_queue_id_decision_queues_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "decision_queues", + "columnsFrom": [ + "queue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_actor_agent_id_agents_id_fk": { + "name": "decision_triage_events_actor_agent_id_agents_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_actor_run_id_heartbeat_runs_id_fk": { + "name": "decision_triage_events_actor_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "actor_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_triage_events_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_triage_events_actor_check": { + "name": "decision_triage_events_actor_check", + "value": "(\n (\"decision_triage_events\".\"actor_type\" = 'agent' AND \"decision_triage_events\".\"actor_agent_id\" IS NOT NULL AND \"decision_triage_events\".\"actor_user_id\" IS NULL)\n OR (\"decision_triage_events\".\"actor_type\" = 'user' AND \"decision_triage_events\".\"actor_agent_id\" IS NULL AND \"decision_triage_events\".\"actor_user_id\" IS NOT NULL)\n OR (\"decision_triage_events\".\"actor_type\" = 'system' AND \"decision_triage_events\".\"actor_agent_id\" IS NULL AND \"decision_triage_events\".\"actor_user_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_training_examples": { + "name": "decision_training_examples", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cutoff_at": { + "name": "cutoff_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "notes_history": { + "name": "notes_history", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "decision_outcome": { + "name": "decision_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scrub_deleted_comments_v1'" + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_training_examples_company_created_at_idx": { + "name": "decision_training_examples_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_training_examples_issue_idx": { + "name": "decision_training_examples_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_training_examples_source_author_uq": { + "name": "decision_training_examples_source_author_uq", + "columns": [ + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_training_examples_company_id_companies_id_fk": { + "name": "decision_training_examples_company_id_companies_id_fk", + "tableFrom": "decision_training_examples", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_training_examples_issue_id_issues_id_fk": { + "name": "decision_training_examples_issue_id_issues_id_fk", + "tableFrom": "decision_training_examples", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_bundles": { + "name": "decision_bundles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_bundles_company_created_at_idx": { + "name": "decision_bundles_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_bundles_company_id_companies_id_fk": { + "name": "decision_bundles_company_id_companies_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_agent_id_agents_id_fk": { + "name": "decision_bundles_origin_agent_id_agents_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_issue_id_issues_id_fk": { + "name": "decision_bundles_origin_issue_id_issues_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_run_id_heartbeat_runs_id_fk": { + "name": "decision_bundles_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_effect_executions": { + "name": "decision_effect_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effect_index": { + "name": "effect_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "effect_type": { + "name": "effect_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_issue_id": { + "name": "target_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claimed'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_log_id": { + "name": "activity_log_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "decision_effect_executions_decision_effect_uq": { + "name": "decision_effect_executions_decision_effect_uq", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effect_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_effect_executions_target_issue_idx": { + "name": "decision_effect_executions_target_issue_idx", + "columns": [ + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_effect_executions_decision_id_decisions_id_fk": { + "name": "decision_effect_executions_decision_id_decisions_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_effect_executions_target_issue_id_issues_id_fk": { + "name": "decision_effect_executions_target_issue_id_issues_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "issues", + "columnsFrom": [ + "target_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_effect_executions_activity_log_id_activity_log_id_fk": { + "name": "decision_effect_executions_activity_log_id_activity_log_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "activity_log", + "columnsFrom": [ + "activity_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_target_issues": { + "name": "decision_target_issues", + "schema": "", + "columns": { + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "decision_target_issues_decision_idx": { + "name": "decision_target_issues_decision_idx", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_target_issues_issue_idx": { + "name": "decision_target_issues_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_target_issues_decision_id_decisions_id_fk": { + "name": "decision_target_issues_decision_id_decisions_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_target_issues_issue_id_issues_id_fk": { + "name": "decision_target_issues_issue_id_issues_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_target_issues_company_id_companies_id_fk": { + "name": "decision_target_issues_company_id_companies_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "decision_target_issues_decision_id_issue_id_pk": { + "name": "decision_target_issues_decision_id_issue_id_pk", + "columns": [ + "decision_id", + "issue_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decisions": { + "name": "decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bundle_id": { + "name": "bundle_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rule_key": { + "name": "rule_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "execution_status": { + "name": "execution_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chosen_option_id": { + "name": "chosen_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_values": { + "name": "input_values", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signed_spec": { + "name": "signed_spec", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_snapshots": { + "name": "target_snapshots", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "continuation_policy": { + "name": "continuation_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decisions_company_status_expires_at_idx": { + "name": "decisions_company_status_expires_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_bundle_idx": { + "name": "decisions_bundle_idx", + "columns": [ + { + "expression": "bundle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_origin_issue_idx": { + "name": "decisions_origin_issue_idx", + "columns": [ + { + "expression": "origin_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_company_idempotency_uq": { + "name": "decisions_company_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"decisions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decisions_company_id_companies_id_fk": { + "name": "decisions_company_id_companies_id_fk", + "tableFrom": "decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_bundle_id_decision_bundles_id_fk": { + "name": "decisions_bundle_id_decision_bundles_id_fk", + "tableFrom": "decisions", + "tableTo": "decision_bundles", + "columnsFrom": [ + "bundle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "decisions_origin_agent_id_agents_id_fk": { + "name": "decisions_origin_agent_id_agents_id_fk", + "tableFrom": "decisions", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_origin_issue_id_issues_id_fk": { + "name": "decisions_origin_issue_id_issues_id_fk", + "tableFrom": "decisions", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_origin_run_id_heartbeat_runs_id_fk": { + "name": "decisions_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_annotation_anchor_snapshots": { + "name": "document_annotation_anchor_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_revision_id": { + "name": "from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "from_revision_number": { + "name": "from_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "to_revision_id": { + "name": "to_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_revision_number": { + "name": "to_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_anchor": { + "name": "previous_anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "next_anchor": { + "name": "next_anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "anchor_state": { + "name": "anchor_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_confidence": { + "name": "anchor_confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_annotation_anchor_snapshots_company_thread_created_at_idx": { + "name": "document_annotation_anchor_snapshots_company_thread_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_anchor_snapshots_company_document_revision_idx": { + "name": "document_annotation_anchor_snapshots_company_document_revision_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_anchor_snapshots_company_id_companies_id_fk": { + "name": "document_annotation_anchor_snapshots_company_id_companies_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_thread_id_document_annotation_threads_id_fk": { + "name": "document_annotation_anchor_snapshots_thread_id_document_annotation_threads_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_annotation_threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_document_id_documents_id_fk": { + "name": "document_annotation_anchor_snapshots_document_id_documents_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_from_revision_id_document_revisions_id_fk": { + "name": "document_annotation_anchor_snapshots_from_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_revisions", + "columnsFrom": [ + "from_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_to_revision_id_document_revisions_id_fk": { + "name": "document_annotation_anchor_snapshots_to_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_revisions", + "columnsFrom": [ + "to_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_annotation_comments": { + "name": "document_annotation_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_annotation_comments_company_thread_created_at_idx": { + "name": "document_annotation_comments_company_thread_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_issue_created_at_idx": { + "name": "document_annotation_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_routine_created_at_idx": { + "name": "document_annotation_comments_company_routine_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_case_created_at_idx": { + "name": "document_annotation_comments_company_case_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_document_created_at_idx": { + "name": "document_annotation_comments_company_document_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_issue_comment_idx": { + "name": "document_annotation_comments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_body_search_idx": { + "name": "document_annotation_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_comments_company_id_companies_id_fk": { + "name": "document_annotation_comments_company_id_companies_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_comments_thread_id_document_annotation_threads_id_fk": { + "name": "document_annotation_comments_thread_id_document_annotation_threads_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "document_annotation_threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_issue_id_issues_id_fk": { + "name": "document_annotation_comments_issue_id_issues_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_routine_id_routines_id_fk": { + "name": "document_annotation_comments_routine_id_routines_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_case_id_cases_id_fk": { + "name": "document_annotation_comments_case_id_cases_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_document_id_documents_id_fk": { + "name": "document_annotation_comments_document_id_documents_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_author_agent_id_agents_id_fk": { + "name": "document_annotation_comments_author_agent_id_agents_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_annotation_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_comments_issue_comment_id_issue_comments_id_fk": { + "name": "document_annotation_comments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_annotation_comments_exactly_one_owner_chk": { + "name": "document_annotation_comments_exactly_one_owner_chk", + "value": "num_nonnulls(\"document_annotation_comments\".\"issue_id\", \"document_annotation_comments\".\"routine_id\", \"document_annotation_comments\".\"case_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.document_annotation_threads": { + "name": "document_annotation_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "anchor_state": { + "name": "anchor_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "original_revision_id": { + "name": "original_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_revision_number": { + "name": "original_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "current_revision_number": { + "name": "current_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "selected_text": { + "name": "selected_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix_text": { + "name": "prefix_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "suffix_text": { + "name": "suffix_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "normalized_start": { + "name": "normalized_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "normalized_end": { + "name": "normalized_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "markdown_start": { + "name": "markdown_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "markdown_end": { + "name": "markdown_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "anchor_confidence": { + "name": "anchor_confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exact'" + }, + "anchor_selector": { + "name": "anchor_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_annotation_threads_company_document_status_idx": { + "name": "document_annotation_threads_company_document_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_issue_status_idx": { + "name": "document_annotation_threads_company_issue_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_routine_status_idx": { + "name": "document_annotation_threads_company_routine_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_case_status_idx": { + "name": "document_annotation_threads_company_case_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_current_revision_open_idx": { + "name": "document_annotation_threads_company_current_revision_open_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_anchor_state_idx": { + "name": "document_annotation_threads_company_anchor_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anchor_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_threads_company_id_companies_id_fk": { + "name": "document_annotation_threads_company_id_companies_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_threads_issue_id_issues_id_fk": { + "name": "document_annotation_threads_issue_id_issues_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_routine_id_routines_id_fk": { + "name": "document_annotation_threads_routine_id_routines_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_case_id_cases_id_fk": { + "name": "document_annotation_threads_case_id_cases_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_document_id_documents_id_fk": { + "name": "document_annotation_threads_document_id_documents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_original_revision_id_document_revisions_id_fk": { + "name": "document_annotation_threads_original_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "document_revisions", + "columnsFrom": [ + "original_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_current_revision_id_document_revisions_id_fk": { + "name": "document_annotation_threads_current_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "document_revisions", + "columnsFrom": [ + "current_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_created_by_agent_id_agents_id_fk": { + "name": "document_annotation_threads_created_by_agent_id_agents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_resolved_by_agent_id_agents_id_fk": { + "name": "document_annotation_threads_resolved_by_agent_id_agents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_annotation_threads_exactly_one_owner_chk": { + "name": "document_annotation_threads_exactly_one_owner_chk", + "value": "num_nonnulls(\"document_annotation_threads\".\"issue_id\", \"document_annotation_threads\".\"routine_id\", \"document_annotation_threads\".\"case_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.document_memberships": { + "name": "document_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_memberships_company_user_starred_idx": { + "name": "document_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_memberships_company_user_document_uq": { + "name": "document_memberships_company_user_document_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_memberships_company_id_companies_id_fk": { + "name": "document_memberships_company_id_companies_id_fk", + "tableFrom": "document_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_memberships_document_id_documents_id_fk": { + "name": "document_memberships_document_id_documents_id_fk", + "tableFrom": "document_memberships", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by_agent_id": { + "name": "locked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "locked_by_user_id": { + "name": "locked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_title_search_idx": { + "name": "documents_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "documents_latest_body_search_idx": { + "name": "documents_latest_body_search_idx", + "columns": [ + { + "expression": "latest_body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_locked_by_agent_id_agents_id_fk": { + "name": "documents_locked_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "locked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_custom_image_setup_sessions": { + "name": "environment_custom_image_setup_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "promoted_template_id": { + "name": "promoted_template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_lease_id": { + "name": "environment_lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starting'" + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_by_agent_id": { + "name": "started_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "base_template_ref": { + "name": "base_template_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_summary": { + "name": "connection_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "connection_secret_ref": { + "name": "connection_secret_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_custom_image_setup_sessions_environment_status_idx": { + "name": "environment_custom_image_setup_sessions_environment_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_environment_active_uq": { + "name": "environment_custom_image_setup_sessions_environment_active_uq", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environment_custom_image_setup_sessions\".\"status\" IN ('starting', 'waiting_for_user', 'capturing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_template_idx": { + "name": "environment_custom_image_setup_sessions_template_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_promoted_template_idx": { + "name": "environment_custom_image_setup_sessions_promoted_template_idx", + "columns": [ + { + "expression": "promoted_template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_expires_idx": { + "name": "environment_custom_image_setup_sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_provider_lease_idx": { + "name": "environment_custom_image_setup_sessions_provider_lease_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_custom_image_setup_sessions_environment_id_environments_id_fk": { + "name": "environment_custom_image_setup_sessions_environment_id_environments_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_setup_sessions_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_promoted_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_setup_sessions_promoted_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "promoted_template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_environment_lease_id_environment_leases_id_fk": { + "name": "environment_custom_image_setup_sessions_environment_lease_id_environment_leases_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_leases", + "columnsFrom": [ + "environment_lease_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_started_by_agent_id_agents_id_fk": { + "name": "environment_custom_image_setup_sessions_started_by_agent_id_agents_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "agents", + "columnsFrom": [ + "started_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_custom_image_templates": { + "name": "environment_custom_image_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_kind": { + "name": "template_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "template_ref": { + "name": "template_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_template_ref": { + "name": "source_template_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_environment_config_fingerprint": { + "name": "source_environment_config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "captured_at": { + "name": "captured_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 + }, + "superseded_by_template_id": { + "name": "superseded_by_template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_custom_image_templates_environment_status_idx": { + "name": "environment_custom_image_templates_environment_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_environment_provider_status_idx": { + "name": "environment_custom_image_templates_environment_provider_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_environment_active_uq": { + "name": "environment_custom_image_templates_environment_active_uq", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environment_custom_image_templates\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_superseded_by_idx": { + "name": "environment_custom_image_templates_superseded_by_idx", + "columns": [ + { + "expression": "superseded_by_template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_last_used_idx": { + "name": "environment_custom_image_templates_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_custom_image_templates_environment_id_environments_id_fk": { + "name": "environment_custom_image_templates_environment_id_environments_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_custom_image_templates_created_by_agent_id_agents_id_fk": { + "name": "environment_custom_image_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_templates_superseded_by_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_templates_superseded_by_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "superseded_by_template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_leases": { + "name": "environment_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "lease_policy": { + "name": "lease_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ephemeral'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_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 + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_status": { + "name": "cleanup_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_leases_company_environment_status_idx": { + "name": "environment_leases_company_environment_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_execution_workspace_idx": { + "name": "environment_leases_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_issue_idx": { + "name": "environment_leases_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_heartbeat_run_idx": { + "name": "environment_leases_heartbeat_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_last_used_idx": { + "name": "environment_leases_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_provider_lease_idx": { + "name": "environment_leases_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_leases_company_id_companies_id_fk": { + "name": "environment_leases_company_id_companies_id_fk", + "tableFrom": "environment_leases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_leases_environment_id_environments_id_fk": { + "name": "environment_leases_environment_id_environments_id_fk", + "tableFrom": "environment_leases", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "environment_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "environment_leases", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_issue_id_issues_id_fk": { + "name": "environment_leases_issue_id_issues_id_fk", + "tableFrom": "environment_leases", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "environment_leases", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environments_status_idx": { + "name": "environments_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_local_driver_idx": { + "name": "environments_local_driver_idx", + "columns": [ + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environments\".\"driver\" = 'local'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_managed_sandbox_idx": { + "name": "environments_managed_sandbox_idx", + "columns": [ + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environments\".\"driver\" = 'sandbox' AND (\"environments\".\"metadata\" ->> 'managedByPaperclip')::boolean = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_name_idx": { + "name": "environments_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspace_runtime_leases": { + "name": "execution_workspace_runtime_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_key": { + "name": "owner_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_issue_id": { + "name": "owner_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_run_id": { + "name": "owner_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_action": { + "name": "last_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "renewed_at": { + "name": "renewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspace_runtime_leases_company_workspace_idx": { + "name": "execution_workspace_runtime_leases_company_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_runtime_leases_company_owner_idx": { + "name": "execution_workspace_runtime_leases_company_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_runtime_leases_expires_at_idx": { + "name": "execution_workspace_runtime_leases_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspace_runtime_leases_company_id_companies_id_fk": { + "name": "execution_workspace_runtime_leases_company_id_companies_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspace_runtime_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_issue_id_issues_id_fk": { + "name": "execution_workspace_runtime_leases_owner_issue_id_issues_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "issues", + "columnsFrom": [ + "owner_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_run_id_heartbeat_runs_id_fk": { + "name": "execution_workspace_runtime_leases_owner_run_id_heartbeat_runs_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "owner_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_agent_id_agents_id_fk": { + "name": "execution_workspace_runtime_leases_owner_agent_id_agents_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "execution_workspace_runtime_leases_execution_workspace_id_unique": { + "name": "execution_workspace_runtime_leases_execution_workspace_id_unique", + "nullsNotDistinct": false, + "columns": [ + "execution_workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspaces": { + "name": "execution_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy_type": { + "name": "strategy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_fs'" + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "derived_from_execution_workspace_id": { + "name": "derived_from_execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_eligible_at": { + "name": "cleanup_eligible_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_reason": { + "name": "cleanup_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspaces_company_project_status_idx": { + "name": "execution_workspaces_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_project_workspace_status_idx": { + "name": "execution_workspaces_company_project_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_source_issue_idx": { + "name": "execution_workspaces_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_last_used_idx": { + "name": "execution_workspaces_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_branch_idx": { + "name": "execution_workspaces_company_branch_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_object_mentions": { + "name": "external_object_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_record_id": { + "name": "source_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "property_key": { + "name": "property_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_text_redacted": { + "name": "matched_text_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sanitized_display_url": { + "name": "sanitized_display_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity_hash": { + "name": "canonical_identity_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity": { + "name": "canonical_identity", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detector_key": { + "name": "detector_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "object_type": { + "name": "object_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exact'" + }, + "created_by_plugin_id": { + "name": "created_by_plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "external_object_mentions_company_source_issue_idx": { + "name": "external_object_mentions_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_object_idx": { + "name": "external_object_mentions_company_object_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_provider_idx": { + "name": "external_object_mentions_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_source_record_uq": { + "name": "external_object_mentions_company_source_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"external_object_mentions\".\"source_record_id\" is not null and \"external_object_mentions\".\"canonical_identity_hash\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_source_null_record_uq": { + "name": "external_object_mentions_company_source_null_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"external_object_mentions\".\"source_record_id\" is null and \"external_object_mentions\".\"canonical_identity_hash\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_object_mentions_company_id_companies_id_fk": { + "name": "external_object_mentions_company_id_companies_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_object_mentions_source_issue_id_issues_id_fk": { + "name": "external_object_mentions_source_issue_id_issues_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_object_mentions_object_id_external_objects_id_fk": { + "name": "external_object_mentions_object_id_external_objects_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "external_objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_object_mentions_created_by_plugin_id_plugins_id_fk": { + "name": "external_object_mentions_created_by_plugin_id_plugins_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "plugins", + "columnsFrom": [ + "created_by_plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_objects": { + "name": "external_objects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "object_type": { + "name": "object_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sanitized_canonical_url": { + "name": "sanitized_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity_hash": { + "name": "canonical_identity_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_key": { + "name": "display_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_key": { + "name": "icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_title": { + "name": "display_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_key": { + "name": "status_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_label": { + "name": "status_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_icon_key": { + "name": "status_icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_category": { + "name": "status_category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "status_tone": { + "name": "status_tone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'neutral'" + }, + "liveness": { + "name": "liveness", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "is_terminal": { + "name": "is_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "remote_version": { + "name": "remote_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_at": { + "name": "last_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_changed_at": { + "name": "last_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_refresh_at": { + "name": "next_refresh_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_started_at": { + "name": "refresh_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "external_objects_company_provider_object_idx": { + "name": "external_objects_company_provider_object_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_provider_status_idx": { + "name": "external_objects_company_provider_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_refresh_idx": { + "name": "external_objects_company_refresh_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_refresh_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_external_id_uq": { + "name": "external_objects_company_external_id_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_identity_uq": { + "name": "external_objects_company_identity_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_objects_company_id_companies_id_fk": { + "name": "external_objects_company_id_companies_id_fk", + "tableFrom": "external_objects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_objects_plugin_id_plugins_id_fk": { + "name": "external_objects_plugin_id_plugins_id_fk", + "tableFrom": "external_objects", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_exports": { + "name": "feedback_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "feedback_vote_id": { + "name": "feedback_vote_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "export_id": { + "name": "export_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-envelope-v2'" + }, + "bundle_version": { + "name": "bundle_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-bundle-v2'" + }, + "payload_version": { + "name": "payload_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-v1'" + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_snapshot": { + "name": "payload_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_summary": { + "name": "target_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "exported_at": { + "name": "exported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_exports_feedback_vote_idx": { + "name": "feedback_exports_feedback_vote_idx", + "columns": [ + { + "expression": "feedback_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_created_idx": { + "name": "feedback_exports_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_status_idx": { + "name": "feedback_exports_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_issue_idx": { + "name": "feedback_exports_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_project_idx": { + "name": "feedback_exports_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_author_idx": { + "name": "feedback_exports_company_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_exports_company_id_companies_id_fk": { + "name": "feedback_exports_company_id_companies_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_exports_feedback_vote_id_feedback_votes_id_fk": { + "name": "feedback_exports_feedback_vote_id_feedback_votes_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "feedback_votes", + "columnsFrom": [ + "feedback_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_issue_id_issues_id_fk": { + "name": "feedback_exports_issue_id_issues_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_project_id_projects_id_fk": { + "name": "feedback_exports_project_id_projects_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_votes": { + "name": "feedback_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_labs": { + "name": "shared_with_labs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_votes_company_issue_idx": { + "name": "feedback_votes_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_issue_target_idx": { + "name": "feedback_votes_issue_target_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_author_idx": { + "name": "feedback_votes_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_company_target_author_idx": { + "name": "feedback_votes_company_target_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_votes_company_id_companies_id_fk": { + "name": "feedback_votes_company_id_companies_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_votes_issue_id_issues_id_fk": { + "name": "feedback_votes_issue_id_issues_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folders": { + "name": "folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_key": { + "name": "system_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "folders_company_kind_position_idx": { + "name": "folders_company_kind_position_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_root_slug_uq": { + "name": "folders_company_kind_root_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"parent_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_parent_slug_uq": { + "name": "folders_company_kind_parent_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"parent_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_system_key_uq": { + "name": "folders_company_kind_system_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "system_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"system_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_parent_position_idx": { + "name": "folders_company_kind_parent_position_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folders_company_id_companies_id_fk": { + "name": "folders_company_id_companies_id_fk", + "tableFrom": "folders", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folders_parent_id_folders_id_fk": { + "name": "folders_parent_id_folders_id_fk", + "tableFrom": "folders", + "tableTo": "folders", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_instance_id": { + "name": "source_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_seq": { + "name": "source_seq", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_payload_sha256": { + "name": "source_payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol_schema_version": { + "name": "protocol_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_run_seq_uq": { + "name": "heartbeat_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": {} + }, + "heartbeat_run_events_run_source_event_uq": { + "name": "heartbeat_run_events_run_source_event_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"heartbeat_run_events\".\"source_event_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_run_source_seq_uq": { + "name": "heartbeat_run_events_run_source_seq_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"heartbeat_run_events\".\"source_instance_id\" is not null and \"heartbeat_run_events\".\"source_seq\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_watchdog_decisions": { + "name": "heartbeat_run_watchdog_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluation_issue_id": { + "name": "evaluation_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_watchdog_decisions_company_run_created_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "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": {} + }, + "heartbeat_run_watchdog_decisions_company_run_snooze_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_snooze_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_watchdog_decisions_company_id_companies_id_fk": { + "name": "heartbeat_run_watchdog_decisions_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk": { + "name": "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "issues", + "columnsFrom": [ + "evaluation_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "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 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "runtime_mode": { + "name": "runtime_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'legacy'" + }, + "runtime_mode_resolver_version": { + "name": "runtime_mode_resolver_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_mode_reason": { + "name": "runtime_mode_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_mode_resolved_at": { + "name": "runtime_mode_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "runner_profile_json": { + "name": "runner_profile_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "runner_instance_id": { + "name": "runner_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "native_session_id": { + "name": "native_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "driver_kind": { + "name": "driver_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_contract_id": { + "name": "completion_contract_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "completion_contract_sha256": { + "name": "completion_contract_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_event_seq": { + "name": "next_event_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "native_phase": { + "name": "native_phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "native_phase_updated_at": { + "name": "native_phase_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_group_id": { + "name": "process_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_at": { + "name": "last_output_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_seq": { + "name": "last_output_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_output_stream": { + "name": "last_output_stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_output_bytes": { + "name": "last_output_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_at": { + "name": "scheduled_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_retry_attempt": { + "name": "scheduled_retry_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_reason": { + "name": "scheduled_retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_comment_status": { + "name": "issue_comment_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_applicable'" + }, + "issue_comment_satisfied_by_comment_id": { + "name": "issue_comment_satisfied_by_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_retry_queued_at": { + "name": "issue_comment_retry_queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "liveness_state": { + "name": "liveness_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "liveness_reason": { + "name": "liveness_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "continuation_attempt": { + "name": "continuation_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_useful_action_at": { + "name": "last_useful_action_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_responsible_user_idx": { + "name": "heartbeat_runs_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_liveness_idx": { + "name": "heartbeat_runs_company_liveness_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "liveness_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_last_output_idx": { + "name": "heartbeat_runs_company_status_last_output_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_output_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_process_started_idx": { + "name": "heartbeat_runs_company_status_process_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "process_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_created_at_desc_idx": { + "name": "heartbeat_runs_company_created_at_desc_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_issue_created_idx": { + "name": "heartbeat_runs_company_ctx_issue_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'issueId')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_task_created_idx": { + "name": "heartbeat_runs_company_ctx_task_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'taskId')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_taskkey_created_idx": { + "name": "heartbeat_runs_company_ctx_taskkey_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'taskKey')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_dismissals": { + "name": "inbox_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dismiss'" + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_dismissals_company_user_idx": { + "name": "inbox_dismissals_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_item_idx": { + "name": "inbox_dismissals_company_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_user_item_idx": { + "name": "inbox_dismissals_company_user_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_dismissals_company_id_companies_id_fk": { + "name": "inbox_dismissals_company_id_companies_id_fk", + "tableFrom": "inbox_dismissals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_grants": { + "name": "connection_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant": { + "name": "provider_tenant", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_secret_refs": { + "name": "credential_secret_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_by_agent_id": { + "name": "revoked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revoked_by_user_id": { + "name": "revoked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "connection_grants_company_connection_idx": { + "name": "connection_grants_company_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_subject_user_idx": { + "name": "connection_grants_subject_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_user_uq": { + "name": "connection_grants_user_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_default_uq": { + "name": "connection_grants_default_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_grants\".\"is_default\" = true and \"connection_grants\".\"kind\" = 'workspace'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connection_grants_company_id_companies_id_fk": { + "name": "connection_grants_company_id_companies_id_fk", + "tableFrom": "connection_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_grants_created_by_agent_id_agents_id_fk": { + "name": "connection_grants_created_by_agent_id_agents_id_fk", + "tableFrom": "connection_grants", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_grants_revoked_by_agent_id_agents_id_fk": { + "name": "connection_grants_revoked_by_agent_id_agents_id_fk", + "tableFrom": "connection_grants", + "tableTo": "agents", + "columnsFrom": [ + "revoked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_grants_company_connection_fk": { + "name": "connection_grants_company_connection_fk", + "tableFrom": "connection_grants", + "tableTo": "tool_connections", + "columnsFrom": [ + "company_id", + "connection_id" + ], + "columnsTo": [ + "company_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "connection_grants_kind_check": { + "name": "connection_grants_kind_check", + "value": "\"connection_grants\".\"kind\" in ('workspace', 'user')" + }, + "connection_grants_status_check": { + "name": "connection_grants_status_check", + "value": "\"connection_grants\".\"status\" in ('active', 'revoked', 'expired', 'needs_reauthorization')" + }, + "connection_grants_subject_check": { + "name": "connection_grants_subject_check", + "value": "(\"connection_grants\".\"kind\" = 'user' and \"connection_grants\".\"subject_user_id\" is not null) or (\"connection_grants\".\"kind\" = 'workspace' and \"connection_grants\".\"subject_user_id\" is null)" + }, + "connection_grants_default_check": { + "name": "connection_grants_default_check", + "value": "\"connection_grants\".\"is_default\" = false or \"connection_grants\".\"kind\" = 'workspace'" + } + }, + "isRLSEnabled": false + }, + "public.connection_token_issuances": { + "name": "connection_token_issuances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_scope": { + "name": "requested_scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "issued_scope": { + "name": "issued_scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "connection_token_issuances_company_created_idx": { + "name": "connection_token_issuances_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_connection_created_idx": { + "name": "connection_token_issuances_connection_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_agent_connection_idx": { + "name": "connection_token_issuances_agent_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_run_idx": { + "name": "connection_token_issuances_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connection_token_issuances_company_id_companies_id_fk": { + "name": "connection_token_issuances_company_id_companies_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_application_id_tool_applications_id_fk": { + "name": "connection_token_issuances_application_id_tool_applications_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_connection_id_tool_connections_id_fk": { + "name": "connection_token_issuances_connection_id_tool_connections_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_agent_id_agents_id_fk": { + "name": "connection_token_issuances_agent_id_agents_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_run_id_heartbeat_runs_id_fk": { + "name": "connection_token_issuances_run_id_heartbeat_runs_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_issue_id_issues_id_fk": { + "name": "connection_token_issuances_issue_id_issues_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_project_id_projects_id_fk": { + "name": "connection_token_issuances_project_id_projects_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "instance_settings_default_environment_id_environments_id_fk": { + "name": "instance_settings_default_environment_id_environments_id_fk", + "tableFrom": "instance_settings", + "tableTo": "environments", + "columnsFrom": [ + "default_environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "on_behalf_of_user_id": { + "name": "on_behalf_of_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_author_agent_id": { + "name": "derived_author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_created_by_run_id": { + "name": "derived_created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_author_source": { + "name": "derived_author_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "presentation": { + "name": "presentation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_by_type": { + "name": "deleted_by_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_by_agent_id": { + "name": "deleted_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deleted_by_user_id": { + "name": "deleted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_by_run_id": { + "name": "deleted_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_body_search_idx": { + "name": "issue_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_on_behalf_of_user_id_user_id_fk": { + "name": "issue_comments_on_behalf_of_user_id_user_id_fk", + "tableFrom": "issue_comments", + "tableTo": "user", + "columnsFrom": [ + "on_behalf_of_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_derived_author_agent_id_agents_id_fk": { + "name": "issue_comments_derived_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "derived_author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_derived_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_derived_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "derived_created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_deleted_by_agent_id_agents_id_fk": { + "name": "issue_comments_deleted_by_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "deleted_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_deleted_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_deleted_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "deleted_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_create_idempotency_keys": { + "name": "issue_create_idempotency_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_create_idempotency_keys_company_key_uq": { + "name": "issue_create_idempotency_keys_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_create_idempotency_keys_issue_idx": { + "name": "issue_create_idempotency_keys_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_create_idempotency_keys_company_created_at_idx": { + "name": "issue_create_idempotency_keys_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_create_idempotency_keys_company_id_companies_id_fk": { + "name": "issue_create_idempotency_keys_company_id_companies_id_fk", + "tableFrom": "issue_create_idempotency_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_create_idempotency_keys_issue_id_issues_id_fk": { + "name": "issue_create_idempotency_keys_issue_id_issues_id_fk", + "tableFrom": "issue_create_idempotency_keys", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_execution_decisions": { + "name": "issue_execution_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_type": { + "name": "stage_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_execution_decisions_company_issue_idx": { + "name": "issue_execution_decisions_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_execution_decisions_stage_idx": { + "name": "issue_execution_decisions_stage_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_execution_decisions_company_id_companies_id_fk": { + "name": "issue_execution_decisions_company_id_companies_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_issue_id_issues_id_fk": { + "name": "issue_execution_decisions_issue_id_issues_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_execution_decisions_actor_agent_id_agents_id_fk": { + "name": "issue_execution_decisions_actor_agent_id_agents_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_inbox_archives": { + "name": "issue_inbox_archives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_by_actor_type": { + "name": "archived_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "archived_by_agent_id": { + "name": "archived_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_by_run_id": { + "name": "archived_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_inbox_archives_company_issue_idx": { + "name": "issue_inbox_archives_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_user_idx": { + "name": "issue_inbox_archives_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_issue_user_idx": { + "name": "issue_inbox_archives_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_inbox_archives_archived_by_agent_id_agents_id_fk": { + "name": "issue_inbox_archives_archived_by_agent_id_agents_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_inbox_archives_archived_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_inbox_archives_archived_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "archived_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "issue_inbox_archives_archived_by_actor_type_check": { + "name": "issue_inbox_archives_archived_by_actor_type_check", + "value": "\"issue_inbox_archives\".\"archived_by_actor_type\" in ('user', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_plan_decompositions": { + "name": "issue_plan_decompositions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_plan_revision_id": { + "name": "accepted_plan_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_interaction_id": { + "name": "accepted_interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'in_flight'" + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_child_count": { + "name": "requested_child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_children": { + "name": "requested_children", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "child_issue_ids": { + "name": "child_issue_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_run_id": { + "name": "owner_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_plan_decompositions_company_source_status_idx": { + "name": "issue_plan_decompositions_company_source_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_plan_decompositions_active_owner_idx": { + "name": "issue_plan_decompositions_active_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"issue_plan_decompositions\".\"status\" = 'in_flight'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_plan_decompositions_source_revision_uq": { + "name": "issue_plan_decompositions_source_revision_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_plan_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_plan_decompositions_company_id_companies_id_fk": { + "name": "issue_plan_decompositions_company_id_companies_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_plan_decompositions_source_issue_id_issues_id_fk": { + "name": "issue_plan_decompositions_source_issue_id_issues_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_plan_decompositions_accepted_plan_revision_id_document_revisions_id_fk": { + "name": "issue_plan_decompositions_accepted_plan_revision_id_document_revisions_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "document_revisions", + "columnsFrom": [ + "accepted_plan_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_plan_decompositions_accepted_interaction_id_issue_thread_interactions_id_fk": { + "name": "issue_plan_decompositions_accepted_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "issue_thread_interactions", + "columnsFrom": [ + "accepted_interaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_plan_decompositions_owner_agent_id_agents_id_fk": { + "name": "issue_plan_decompositions_owner_agent_id_agents_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_plan_decompositions_owner_run_id_heartbeat_runs_id_fk": { + "name": "issue_plan_decompositions_owner_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "owner_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_recovery_actions": { + "name": "issue_recovery_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recovery_issue_id": { + "name": "recovery_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_owner_agent_id": { + "name": "previous_owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "return_owner_agent_id": { + "name": "return_owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cause": { + "name": "cause", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wake_policy": { + "name": "wake_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "monitor_policy": { + "name": "monitor_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timeout_at": { + "name": "timeout_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_recovery_actions_company_source_status_idx": { + "name": "issue_recovery_actions_company_source_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_company_owner_status_idx": { + "name": "issue_recovery_actions_company_owner_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_company_recovery_issue_idx": { + "name": "issue_recovery_actions_company_recovery_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recovery_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_active_source_uq": { + "name": "issue_recovery_actions_active_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_recovery_actions\".\"status\" in ('active', 'escalated')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_active_fingerprint_uq": { + "name": "issue_recovery_actions_active_fingerprint_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cause", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_recovery_actions\".\"status\" in ('active', 'escalated')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_recovery_actions_company_id_companies_id_fk": { + "name": "issue_recovery_actions_company_id_companies_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_recovery_actions_source_issue_id_issues_id_fk": { + "name": "issue_recovery_actions_source_issue_id_issues_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_recovery_actions_recovery_issue_id_issues_id_fk": { + "name": "issue_recovery_actions_recovery_issue_id_issues_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "issues", + "columnsFrom": [ + "recovery_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_previous_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_previous_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "previous_owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_return_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_return_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "return_owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_reference_mentions": { + "name": "issue_reference_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_issue_id": { + "name": "target_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_record_id": { + "name": "source_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_text": { + "name": "matched_text", + "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": { + "issue_reference_mentions_company_source_issue_idx": { + "name": "issue_reference_mentions_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_target_issue_idx": { + "name": "issue_reference_mentions_company_target_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_issue_pair_idx": { + "name": "issue_reference_mentions_company_issue_pair_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_record_uq": { + "name": "issue_reference_mentions_company_source_mention_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_null_record_uq": { + "name": "issue_reference_mentions_company_source_mention_null_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_reference_mentions_company_id_companies_id_fk": { + "name": "issue_reference_mentions_company_id_companies_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_reference_mentions_source_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_source_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_reference_mentions_target_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_target_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "target_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_relations": { + "name": "issue_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "related_issue_id": { + "name": "related_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_relations_company_issue_idx": { + "name": "issue_relations_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_related_issue_idx": { + "name": "issue_relations_company_related_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_type_idx": { + "name": "issue_relations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_edge_uq": { + "name": "issue_relations_company_edge_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "related_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_thread_interactions": { + "name": "issue_thread_interactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "continuation_policy": { + "name": "continuation_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'wake_assignee'" + }, + "requested_resolver_policy": { + "name": "requested_resolver_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'anyone'" + }, + "effective_resolver_policy": { + "name": "effective_resolver_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'anyone'" + }, + "resolver_policy_provenance": { + "name": "resolver_policy_provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherited'" + }, + "effective_resolver_policy_source": { + "name": "effective_resolver_policy_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_comment_id": { + "name": "source_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "addressee_agent_id": { + "name": "addressee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_run_id": { + "name": "resolved_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_thread_interactions_issue_idx": { + "name": "issue_thread_interactions_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_created_at_idx": { + "name": "issue_thread_interactions_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_status_idx": { + "name": "issue_thread_interactions_company_issue_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_idempotency_uq": { + "name": "issue_thread_interactions_company_issue_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_thread_interactions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_source_comment_idx": { + "name": "issue_thread_interactions_source_comment_idx", + "columns": [ + { + "expression": "source_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_addressee_agent_idx": { + "name": "issue_thread_interactions_addressee_agent_idx", + "columns": [ + { + "expression": "addressee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_thread_interactions_company_id_companies_id_fk": { + "name": "issue_thread_interactions_company_id_companies_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_issue_id_issues_id_fk": { + "name": "issue_thread_interactions_issue_id_issues_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_comment_id_issue_comments_id_fk": { + "name": "issue_thread_interactions_source_comment_id_issue_comments_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issue_comments", + "columnsFrom": [ + "source_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk": { + "name": "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_created_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_addressee_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_addressee_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "addressee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_resolved_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_resolved_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_resolved_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_thread_interactions_resolved_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "resolved_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_hold_members": { + "name": "issue_tree_hold_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "issue_identifier": { + "name": "issue_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_status": { + "name": "issue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_run_id": { + "name": "active_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_run_status": { + "name": "active_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skipped": { + "name": "skipped", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_hold_members_hold_issue_uq": { + "name": "issue_tree_hold_members_hold_issue_uq", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_company_issue_idx": { + "name": "issue_tree_hold_members_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_hold_depth_idx": { + "name": "issue_tree_hold_members_hold_depth_idx", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "depth", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_hold_members_company_id_companies_id_fk": { + "name": "issue_tree_hold_members_company_id_companies_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk": { + "name": "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issue_tree_holds", + "columnsFrom": [ + "hold_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_parent_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_parent_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_assignee_agent_id_agents_id_fk": { + "name": "issue_tree_hold_members_assignee_agent_id_agents_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "active_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_holds": { + "name": "issue_tree_holds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_issue_id": { + "name": "root_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_policy": { + "name": "release_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by_actor_type": { + "name": "released_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_agent_id": { + "name": "released_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_by_user_id": { + "name": "released_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_run_id": { + "name": "released_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_metadata": { + "name": "release_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_holds_company_root_status_idx": { + "name": "issue_tree_holds_company_root_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_holds_company_status_mode_idx": { + "name": "issue_tree_holds_company_status_mode_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_holds_company_id_companies_id_fk": { + "name": "issue_tree_holds_company_id_companies_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_holds_root_issue_id_issues_id_fk": { + "name": "issue_tree_holds_root_issue_id_issues_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "issues", + "columnsFrom": [ + "root_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_released_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "released_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "released_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_watchdogs": { + "name": "issue_watchdogs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchdog_agent_id": { + "name": "watchdog_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "watchdog_issue_id": { + "name": "watchdog_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_observed_fingerprint": { + "name": "last_observed_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_reviewed_fingerprint": { + "name": "last_reviewed_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_observed_stop_snapshot": { + "name": "last_observed_stop_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_reviewed_stop_snapshot": { + "name": "last_reviewed_stop_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_completed_at": { + "name": "last_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trigger_count": { + "name": "trigger_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_run_id": { + "name": "updated_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_watchdogs_company_issue_uq": { + "name": "issue_watchdogs_company_issue_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_status_idx": { + "name": "issue_watchdogs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_agent_idx": { + "name": "issue_watchdogs_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchdog_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_watchdog_issue_uq": { + "name": "issue_watchdogs_company_watchdog_issue_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchdog_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_watchdogs\".\"watchdog_issue_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_watchdogs_company_id_companies_id_fk": { + "name": "issue_watchdogs_company_id_companies_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_watchdogs_issue_id_issues_id_fk": { + "name": "issue_watchdogs_issue_id_issues_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_watchdogs_watchdog_agent_id_agents_id_fk": { + "name": "issue_watchdogs_watchdog_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "watchdog_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_watchdogs_watchdog_issue_id_issues_id_fk": { + "name": "issue_watchdogs_watchdog_issue_id_issues_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "issues", + "columnsFrom": [ + "watchdog_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_created_by_agent_id_agents_id_fk": { + "name": "issue_watchdogs_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_watchdogs_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_updated_by_agent_id_agents_id_fk": { + "name": "issue_watchdogs_updated_by_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_updated_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_watchdogs_updated_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "updated_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_work_products": { + "name": "issue_work_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_service_id": { + "name": "runtime_service_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_work_products_company_issue_type_idx": { + "name": "issue_work_products_company_issue_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_execution_workspace_type_idx": { + "name": "issue_work_products_company_execution_workspace_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_provider_external_id_idx": { + "name": "issue_work_products_company_provider_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_updated_idx": { + "name": "issue_work_products_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_work_products_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issue_work_products_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk": { + "name": "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "workspace_runtime_services", + "columnsFrom": [ + "runtime_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_work_products_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "status_version": { + "name": "status_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_status_decision_id": { + "name": "last_status_decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "work_mode": { + "name": "work_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "harness_kind": { + "name": "harness_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "review_policy": { + "name": "review_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_fingerprint": { + "name": "origin_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_policy": { + "name": "execution_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_state": { + "name": "execution_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "monitor_next_check_at": { + "name": "monitor_next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_wake_requested_at": { + "name": "monitor_wake_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_last_triggered_at": { + "name": "monitor_last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_attempt_count": { + "name": "monitor_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "monitor_notes": { + "name": "monitor_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monitor_scheduled_by": { + "name": "monitor_scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "unblock_descriptor": { + "name": "unblock_descriptor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "blocked_transition_at": { + "name": "blocked_transition_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_owner_notified_at": { + "name": "blocked_owner_notified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_harness_kind_idx": { + "name": "issues_company_harness_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "harness_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_responsible_user_idx": { + "name": "issues_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_origin_idx": { + "name": "issues_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_workspace_idx": { + "name": "issues_company_project_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_execution_workspace_idx": { + "name": "issues_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_monitor_due_idx": { + "name": "issues_company_monitor_due_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "monitor_next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_updated_idx": { + "name": "issues_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_created_idx": { + "name": "issues_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_open_normalized_title_created_idx": { + "name": "issues_open_normalized_title_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(regexp_replace(btrim(\"title\"), '\\s+', ' ', 'g'))", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"issues\".\"hidden_at\" is null and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_priority_idx": { + "name": "issues_company_priority_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_title_search_idx": { + "name": "issues_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_identifier_search_idx": { + "name": "issues_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_description_search_idx": { + "name": "issues_description_search_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_open_routine_execution_uq": { + "name": "issues_open_routine_execution_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'routine_execution'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"execution_run_id\" is not null\n and \"issues\".\"status\" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_incident_uq": { + "name": "issues_active_liveness_recovery_incident_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_leaf_uq": { + "name": "issues_active_liveness_recovery_leaf_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_fingerprint\" <> 'default'\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stale_run_evaluation_uq": { + "name": "issues_active_stale_run_evaluation_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stale_active_run_evaluation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_task_watchdog_uq": { + "name": "issues_active_task_watchdog_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'task_watchdog'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_productivity_review_uq": { + "name": "issues_active_productivity_review_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'issue_productivity_review'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stranded_issue_recovery_uq": { + "name": "issues_active_stranded_issue_recovery_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stranded_issue_recovery'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_onboarding_first_task_uq": { + "name": "issues_onboarding_first_task_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'onboarding_first_task'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_user_uq": { + "name": "join_requests_pending_human_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requesting_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"requesting_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_email_uq": { + "name": "join_requests_pending_human_email_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"request_email_snapshot\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"request_email_snapshot\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.managed_agent_profiles": { + "name": "managed_agent_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_key": { + "name": "profile_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'anthropic_managed_agents'" + }, + "anthropic_agent_id": { + "name": "anthropic_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beta_version": { + "name": "beta_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'managed-agents-2026-04-01'" + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-sonnet-5'" + }, + "default_max_list_cost_cents": { + "name": "default_max_list_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retention_acknowledged": { + "name": "retention_acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "qualification": { + "name": "qualification", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "qualified_at": { + "name": "qualified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "managed_agent_profiles_company_idx": { + "name": "managed_agent_profiles_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "managed_agent_profiles_company_key_uq": { + "name": "managed_agent_profiles_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "managed_agent_profiles_company_resource_uq": { + "name": "managed_agent_profiles_company_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anthropic_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "managed_agent_profiles_company_id_companies_id_fk": { + "name": "managed_agent_profiles_company_id_companies_id_fk", + "tableFrom": "managed_agent_profiles", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "managed_agent_profiles_api_key_secret_id_company_secrets_id_fk": { + "name": "managed_agent_profiles_api_key_secret_id_company_secrets_id_fk", + "tableFrom": "managed_agent_profiles", + "tableTo": "company_secrets", + "columnsFrom": [ + "api_key_secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "managed_agent_profiles_service_check": { + "name": "managed_agent_profiles_service_check", + "value": "\"managed_agent_profiles\".\"service\" = 'anthropic_managed_agents'" + }, + "managed_agent_profiles_beta_check": { + "name": "managed_agent_profiles_beta_check", + "value": "\"managed_agent_profiles\".\"beta_version\" = 'managed-agents-2026-04-01'" + }, + "managed_agent_profiles_positive_budget_check": { + "name": "managed_agent_profiles_positive_budget_check", + "value": "\"managed_agent_profiles\".\"default_max_list_cost_cents\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.native_run_finalizations": { + "name": "native_run_finalizations", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_owner": { + "name": "lease_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_id": { + "name": "result_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assessment_id": { + "name": "assessment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_detail": { + "name": "failure_detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "native_run_finalizations_run_id_heartbeat_runs_id_fk": { + "name": "native_run_finalizations_run_id_heartbeat_runs_id_fk", + "tableFrom": "native_run_finalizations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "native_run_finalizations_company_id_companies_id_fk": { + "name": "native_run_finalizations_company_id_companies_id_fk", + "tableFrom": "native_run_finalizations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "native_run_finalizations_issue_id_issues_id_fk": { + "name": "native_run_finalizations_issue_id_issues_id_fk", + "tableFrom": "native_run_finalizations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "native_run_finalizations_result_id_native_run_results_id_fk": { + "name": "native_run_finalizations_result_id_native_run_results_id_fk", + "tableFrom": "native_run_finalizations", + "tableTo": "native_run_results", + "columnsFrom": [ + "result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_run_results": { + "name": "native_run_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_contract_id": { + "name": "completion_contract_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "caller_result_id": { + "name": "caller_result_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caller_dedupe_key": { + "name": "caller_dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_fingerprint": { + "name": "server_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_status": { + "name": "schema_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rejection_code": { + "name": "rejection_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "canonical_sha256": { + "name": "canonical_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "native_run_results_run_fingerprint_uq": { + "name": "native_run_results_run_fingerprint_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "server_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "native_run_results_run_caller_result_uq": { + "name": "native_run_results_run_caller_result_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "caller_result_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "native_run_results_run_caller_dedupe_uq": { + "name": "native_run_results_run_caller_dedupe_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "caller_dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "native_run_results_company_id_companies_id_fk": { + "name": "native_run_results_company_id_companies_id_fk", + "tableFrom": "native_run_results", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "native_run_results_issue_id_issues_id_fk": { + "name": "native_run_results_issue_id_issues_id_fk", + "tableFrom": "native_run_results", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "native_run_results_run_id_heartbeat_runs_id_fk": { + "name": "native_run_results_run_id_heartbeat_runs_id_fk", + "tableFrom": "native_run_results", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "native_run_results_completion_contract_id_completion_contracts_id_fk": { + "name": "native_run_results_completion_contract_id_completion_contracts_id_fk", + "tableFrom": "native_run_results", + "tableTo": "completion_contracts", + "columnsFrom": [ + "completion_contract_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_automation_executions": { + "name": "pipeline_automation_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggering_event_id": { + "name": "triggering_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_issue_id": { + "name": "execution_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retry_of_execution_id": { + "name": "retry_of_execution_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_automation_executions_idempotency_uq": { + "name": "pipeline_automation_executions_idempotency_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "triggering_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_company_case_idx": { + "name": "pipeline_automation_executions_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_routine_idx": { + "name": "pipeline_automation_executions_routine_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_execution_issue_idx": { + "name": "pipeline_automation_executions_execution_issue_idx", + "columns": [ + { + "expression": "execution_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_retry_of_execution_idx": { + "name": "pipeline_automation_executions_retry_of_execution_idx", + "columns": [ + { + "expression": "retry_of_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_automation_executions_company_id_companies_id_fk": { + "name": "pipeline_automation_executions_company_id_companies_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_case_id_pipeline_cases_id_fk": { + "name": "pipeline_automation_executions_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_routine_id_routines_id_fk": { + "name": "pipeline_automation_executions_routine_id_routines_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_execution_issue_id_issues_id_fk": { + "name": "pipeline_automation_executions_execution_issue_id_issues_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "issues", + "columnsFrom": [ + "execution_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_automation_executions_status_check": { + "name": "pipeline_automation_executions_status_check", + "value": "\"pipeline_automation_executions\".\"status\" in ('succeeded', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_blockers": { + "name": "pipeline_case_blockers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_by_case_id": { + "name": "blocked_by_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_case_blockers_case_blocked_by_uq": { + "name": "pipeline_case_blockers_case_blocked_by_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blocked_by_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_blockers_blocked_by_idx": { + "name": "pipeline_case_blockers_blocked_by_idx", + "columns": [ + { + "expression": "blocked_by_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_blockers_company_case_idx": { + "name": "pipeline_case_blockers_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_blockers_company_id_companies_id_fk": { + "name": "pipeline_case_blockers_company_id_companies_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_blockers_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_blockers_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_blockers_blocked_by_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_blockers_blocked_by_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "blocked_by_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_blockers_no_self_block_check": { + "name": "pipeline_case_blockers_no_self_block_check", + "value": "\"pipeline_case_blockers\".\"case_id\" <> \"pipeline_case_blockers\".\"blocked_by_case_id\"" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_documents": { + "name": "pipeline_case_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_case_documents_company_case_key_uq": { + "name": "pipeline_case_documents_company_case_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_documents_document_uq": { + "name": "pipeline_case_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_documents_company_case_updated_idx": { + "name": "pipeline_case_documents_company_case_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_documents_company_id_companies_id_fk": { + "name": "pipeline_case_documents_company_id_companies_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_documents_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_documents_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_documents_document_id_documents_id_fk": { + "name": "pipeline_case_documents_document_id_documents_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_case_events": { + "name": "pipeline_case_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "from_stage_id": { + "name": "from_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_stage_id": { + "name": "to_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_case_events_case_created_idx": { + "name": "pipeline_case_events_case_created_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_events_company_case_idx": { + "name": "pipeline_case_events_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_events_company_id_companies_id_fk": { + "name": "pipeline_case_events_company_id_companies_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_events_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_events_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_events_actor_agent_id_agents_id_fk": { + "name": "pipeline_case_events_actor_agent_id_agents_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_case_events_from_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_case_events_from_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "from_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_case_events_to_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_case_events_to_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "to_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_events_type_check": { + "name": "pipeline_case_events_type_check", + "value": "\"pipeline_case_events\".\"type\" in (\n 'ingested',\n 'updated',\n 'claimed',\n 'lease_released',\n 'lease_expired',\n 'transitioned',\n 'transition_forced',\n 'transition_suggested',\n 'suggestion_resolved',\n 'review_decided',\n 'conversation_opened',\n 'issue_linked',\n 'issue_unlinked',\n 'automation_executed',\n 'automation_failed',\n 'automation_retry_requested',\n 'automation_effects_retired',\n 'automation_retry_dispatched',\n 'blockers_set',\n 'blockers_resolved',\n 'children_terminal',\n 'upstream_drift',\n 'drift_acknowledged'\n )" + }, + "pipeline_case_events_actor_type_check": { + "name": "pipeline_case_events_actor_type_check", + "value": "\"pipeline_case_events\".\"actor_type\" in ('user', 'agent', 'system')" + }, + "pipeline_case_events_agent_run_check": { + "name": "pipeline_case_events_agent_run_check", + "value": "\"pipeline_case_events\".\"actor_type\" <> 'agent' or \"pipeline_case_events\".\"run_id\" is not null" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_issue_links": { + "name": "pipeline_case_issue_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "automation_attempt_id": { + "name": "automation_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_by_attempt_id": { + "name": "retired_by_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_reason": { + "name": "retired_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_case_issue_links_case_issue_uq": { + "name": "pipeline_case_issue_links_case_issue_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_issue_idx": { + "name": "pipeline_case_issue_links_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_company_case_idx": { + "name": "pipeline_case_issue_links_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_automation_attempt_idx": { + "name": "pipeline_case_issue_links_automation_attempt_idx", + "columns": [ + { + "expression": "automation_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_issue_links_company_id_companies_id_fk": { + "name": "pipeline_case_issue_links_company_id_companies_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_issue_links_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_issue_links_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_issue_links_issue_id_issues_id_fk": { + "name": "pipeline_case_issue_links_issue_id_issues_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_issue_links_role_check": { + "name": "pipeline_case_issue_links_role_check", + "value": "\"pipeline_case_issue_links\".\"role\" in ('origin', 'conversation', 'work', 'automation')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_cases": { + "name": "pipeline_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_key": { + "name": "case_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "workspace_ref": { + "name": "workspace_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_case_id": { + "name": "parent_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_case_version": { + "name": "parent_case_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_key": { + "name": "request_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_attempt_id": { + "name": "automation_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "pending_suggestion": { + "name": "pending_suggestion", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lease_owner_type": { + "name": "lease_owner_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_agent_id": { + "name": "lease_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_user_id": { + "name": "lease_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_kind": { + "name": "terminal_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_by_attempt_id": { + "name": "retired_by_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_reason": { + "name": "retired_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hidden_from_board_at": { + "name": "hidden_from_board_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "child_count": { + "name": "child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "terminal_child_count": { + "name": "terminal_child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_cases_pipeline_case_key_uq": { + "name": "pipeline_cases_pipeline_case_key_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_parent_request_key_uq": { + "name": "pipeline_cases_parent_request_key_uq", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"pipeline_cases\".\"request_key\" is not null and \"pipeline_cases\".\"retired_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_company_idx": { + "name": "pipeline_cases_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_pipeline_stage_idx": { + "name": "pipeline_cases_pipeline_stage_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_parent_idx": { + "name": "pipeline_cases_parent_idx", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_automation_attempt_idx": { + "name": "pipeline_cases_automation_attempt_idx", + "columns": [ + { + "expression": "automation_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_retired_idx": { + "name": "pipeline_cases_retired_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retired_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_lease_expires_idx": { + "name": "pipeline_cases_lease_expires_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pipeline_cases\".\"lease_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_cases_company_id_companies_id_fk": { + "name": "pipeline_cases_company_id_companies_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_cases_pipeline_id_pipelines_id_fk": { + "name": "pipeline_cases_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_cases_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_cases_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pipeline_cases_parent_case_id_pipeline_cases_id_fk": { + "name": "pipeline_cases_parent_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "parent_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_cases_lease_agent_id_agents_id_fk": { + "name": "pipeline_cases_lease_agent_id_agents_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "agents", + "columnsFrom": [ + "lease_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_cases_created_by_agent_id_agents_id_fk": { + "name": "pipeline_cases_created_by_agent_id_agents_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_cases_terminal_kind_check": { + "name": "pipeline_cases_terminal_kind_check", + "value": "\"pipeline_cases\".\"terminal_kind\" is null or \"pipeline_cases\".\"terminal_kind\" in ('done', 'cancelled')" + }, + "pipeline_cases_lease_owner_type_check": { + "name": "pipeline_cases_lease_owner_type_check", + "value": "\"pipeline_cases\".\"lease_owner_type\" is null or \"pipeline_cases\".\"lease_owner_type\" in ('user', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_documents": { + "name": "pipeline_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_documents_company_pipeline_key_uq": { + "name": "pipeline_documents_company_pipeline_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_documents_document_uq": { + "name": "pipeline_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_documents_company_pipeline_updated_idx": { + "name": "pipeline_documents_company_pipeline_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_documents_company_id_companies_id_fk": { + "name": "pipeline_documents_company_id_companies_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_documents_pipeline_id_pipelines_id_fk": { + "name": "pipeline_documents_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_documents_document_id_documents_id_fk": { + "name": "pipeline_documents_document_id_documents_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_stages": { + "name": "pipeline_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_stages_pipeline_key_uq": { + "name": "pipeline_stages_pipeline_key_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_stages_pipeline_position_idx": { + "name": "pipeline_stages_pipeline_position_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_stages_pipeline_id_pipelines_id_fk": { + "name": "pipeline_stages_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_stages", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_stages_kind_check": { + "name": "pipeline_stages_kind_check", + "value": "\"pipeline_stages\".\"kind\" in ('working', 'review', 'done', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_transitions": { + "name": "pipeline_transitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_stage_id": { + "name": "from_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_stage_id": { + "name": "to_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "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": { + "pipeline_transitions_pipeline_edge_uq": { + "name": "pipeline_transitions_pipeline_edge_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_transitions_pipeline_from_idx": { + "name": "pipeline_transitions_pipeline_from_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_transitions_pipeline_to_idx": { + "name": "pipeline_transitions_pipeline_to_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_transitions_pipeline_id_pipelines_id_fk": { + "name": "pipeline_transitions_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_transitions_from_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_transitions_from_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "from_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_transitions_to_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_transitions_to_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "to_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipelines": { + "name": "pipelines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enforce_transitions": { + "name": "enforce_transitions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipelines_company_key_uq": { + "name": "pipelines_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipelines_company_idx": { + "name": "pipelines_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipelines_company_project_idx": { + "name": "pipelines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipelines_company_id_companies_id_fk": { + "name": "pipelines_company_id_companies_id_fk", + "tableFrom": "pipelines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipelines_project_id_projects_id_fk": { + "name": "pipelines_project_id_projects_id_fk", + "tableFrom": "pipelines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipelines_created_by_agent_id_agents_id_fk": { + "name": "pipelines_created_by_agent_id_agents_id_fk", + "tableFrom": "pipelines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_company_idx": { + "name": "plugin_config_plugin_company_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_config_company_id_companies_id_fk": { + "name": "plugin_config_company_id_companies_id_fk", + "tableFrom": "plugin_config", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_database_namespaces": { + "name": "plugin_database_namespaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_mode": { + "name": "namespace_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'schema'" + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_database_namespaces_plugin_idx": { + "name": "plugin_database_namespaces_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_namespace_idx": { + "name": "plugin_database_namespaces_namespace_idx", + "columns": [ + { + "expression": "namespace_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_status_idx": { + "name": "plugin_database_namespaces_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_database_namespaces_plugin_id_plugins_id_fk": { + "name": "plugin_database_namespaces_plugin_id_plugins_id_fk", + "tableFrom": "plugin_database_namespaces", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_company_idx": { + "name": "plugin_entities_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_entities_company_id_companies_id_fk": { + "name": "plugin_entities_company_id_companies_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "nullsNotDistinct": true, + "columns": [ + "company_id", + "plugin_id", + "entity_type", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_company_idx": { + "name": "plugin_job_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_company_id_companies_id_fk": { + "name": "plugin_job_runs_company_id_companies_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_company_idx": { + "name": "plugin_logs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_logs_company_id_companies_id_fk": { + "name": "plugin_logs_company_id_companies_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_managed_resources": { + "name": "plugin_managed_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "defaults_json": { + "name": "defaults_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_managed_resources_company_idx": { + "name": "plugin_managed_resources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_plugin_idx": { + "name": "plugin_managed_resources_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_resource_idx": { + "name": "plugin_managed_resources_resource_idx", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_company_plugin_resource_uq": { + "name": "plugin_managed_resources_company_plugin_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_managed_resources_company_id_companies_id_fk": { + "name": "plugin_managed_resources_company_id_companies_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_managed_resources_plugin_id_plugins_id_fk": { + "name": "plugin_managed_resources_plugin_id_plugins_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_key": { + "name": "migration_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "plugin_migrations_plugin_key_idx": { + "name": "plugin_migrations_plugin_key_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_plugin_idx": { + "name": "plugin_migrations_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_status_idx": { + "name": "plugin_migrations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_company_idx": { + "name": "plugin_webhook_deliveries_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_webhook_deliveries_company_id_companies_id_fk": { + "name": "plugin_webhook_deliveries_company_id_companies_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_memberships": { + "name": "project_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'joined'" + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_memberships_company_user_idx": { + "name": "project_memberships_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_company_user_starred_idx": { + "name": "project_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_project_idx": { + "name": "project_memberships_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_company_user_project_uq": { + "name": "project_memberships_company_user_project_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_memberships_company_id_companies_id_fk": { + "name": "project_memberships_company_id_companies_id_fk", + "tableFrom": "project_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_memberships_project_id_projects_id_fk": { + "name": "project_memberships_project_id_projects_id_fk", + "tableFrom": "project_memberships", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_trace_records": { + "name": "provider_trace_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'capturing'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trace_ref": { + "name": "trace_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "frame_count": { + "name": "frame_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "byte_count": { + "name": "byte_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "digest": { + "name": "digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_trace_records_run_unique": { + "name": "provider_trace_records_run_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_trace_records_expiry_idx": { + "name": "provider_trace_records_expiry_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_trace_records_company_created_idx": { + "name": "provider_trace_records_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_trace_records_company_id_companies_id_fk": { + "name": "provider_trace_records_company_id_companies_id_fk", + "tableFrom": "provider_trace_records", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "provider_trace_records_run_id_heartbeat_runs_id_fk": { + "name": "provider_trace_records_run_id_heartbeat_runs_id_fk", + "tableFrom": "provider_trace_records", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_agent_profiles": { + "name": "remote_agent_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_key": { + "name": "profile_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "credential_secret_id": { + "name": "credential_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retention_acknowledged": { + "name": "retention_acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "qualification": { + "name": "qualification", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "qualified_at": { + "name": "qualified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_agent_profiles_company_idx": { + "name": "remote_agent_profiles_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_agent_profiles_company_key_uq": { + "name": "remote_agent_profiles_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_agent_profiles_company_id_companies_id_fk": { + "name": "remote_agent_profiles_company_id_companies_id_fk", + "tableFrom": "remote_agent_profiles", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "remote_agent_profiles_credential_secret_id_company_secrets_id_fk": { + "name": "remote_agent_profiles_credential_secret_id_company_secrets_id_fk", + "tableFrom": "remote_agent_profiles", + "tableTo": "company_secrets", + "columnsFrom": [ + "credential_secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "remote_agent_profiles_service_check": { + "name": "remote_agent_profiles_service_check", + "value": "\"remote_agent_profiles\".\"service\" in ('anthropic_managed_agents', 'aws_bedrock_agentcore_harness')" + } + }, + "isRLSEnabled": false + }, + "public.routine_documents": { + "name": "routine_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_documents_company_routine_key_uq": { + "name": "routine_documents_company_routine_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_documents_document_uq": { + "name": "routine_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_documents_company_routine_updated_idx": { + "name": "routine_documents_company_routine_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_documents_company_id_companies_id_fk": { + "name": "routine_documents_company_id_companies_id_fk", + "tableFrom": "routine_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routine_documents_routine_id_routines_id_fk": { + "name": "routine_documents_routine_id_routines_id_fk", + "tableFrom": "routine_documents", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_documents_document_id_documents_id_fk": { + "name": "routine_documents_document_id_documents_id_fk", + "tableFrom": "routine_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_revisions": { + "name": "routine_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "restored_from_revision_id": { + "name": "restored_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_revisions_routine_revision_uq": { + "name": "routine_revisions_routine_revision_uq", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_revisions_company_routine_created_idx": { + "name": "routine_revisions_company_routine_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_revisions_company_responsible_user_idx": { + "name": "routine_revisions_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_revisions_company_id_companies_id_fk": { + "name": "routine_revisions_company_id_companies_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_revisions_routine_id_routines_id_fk": { + "name": "routine_revisions_routine_id_routines_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_revisions_restored_from_revision_id_routine_revisions_id_fk": { + "name": "routine_revisions_restored_from_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "routine_revisions", + "columnsFrom": [ + "restored_from_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_revisions_created_by_agent_id_agents_id_fk": { + "name": "routine_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "routine_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "routine_revision_id": { + "name": "routine_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dispatch_fingerprint": { + "name": "dispatch_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_issue_id": { + "name": "linked_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "coalesced_into_run_id": { + "name": "coalesced_into_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_runs_company_routine_idx": { + "name": "routine_runs_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_revision_idx": { + "name": "routine_runs_revision_idx", + "columns": [ + { + "expression": "routine_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_company_responsible_user_idx": { + "name": "routine_runs_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idx": { + "name": "routine_runs_trigger_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_dispatch_fingerprint_idx": { + "name": "routine_runs_dispatch_fingerprint_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatch_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idempotency_idx": { + "name": "routine_runs_trigger_idempotency_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_routine_revision_id_routine_revisions_id_fk": { + "name": "routine_runs_routine_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_revisions", + "columnsFrom": [ + "routine_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "tableTo": "issues", + "columnsFrom": [ + "linked_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_triggers": { + "name": "routine_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "signing_mode": { + "name": "signing_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replay_window_sec": { + "name": "replay_window_sec", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_triggers_company_routine_idx": { + "name": "routine_triggers_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_company_kind_idx": { + "name": "routine_triggers_company_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'coalesce_if_active'" + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip_missed'" + }, + "activity_gate_policy": { + "name": "activity_gate_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'always'" + }, + "activity_gate_scope": { + "name": "activity_gate_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routines_company_status_idx": { + "name": "routines_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_assignee_idx": { + "name": "routines_company_assignee_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_project_idx": { + "name": "routines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_folder_idx": { + "name": "routines_company_folder_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_responsible_user_idx": { + "name": "routines_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_origin_idx": { + "name": "routines_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_folder_id_folders_id_fk": { + "name": "routines_folder_id_folders_id_fk", + "tableFrom": "routines", + "tableTo": "folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_access_events": { + "name": "secret_access_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_owner_user_id": { + "name": "credential_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_subject_type": { + "name": "credential_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_subject_id": { + "name": "credential_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumer_type": { + "name": "consumer_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_id": { + "name": "consumer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_access_events_company_created_idx": { + "name": "secret_access_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_secret_created_idx": { + "name": "secret_access_events_secret_created_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_user_definition_created_idx": { + "name": "secret_access_events_user_definition_created_idx", + "columns": [ + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_company_credential_owner_idx": { + "name": "secret_access_events_company_credential_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_consumer_idx": { + "name": "secret_access_events_consumer_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_run_idx": { + "name": "secret_access_events_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_access_events_company_id_companies_id_fk": { + "name": "secret_access_events_company_id_companies_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "secret_access_events_secret_id_company_secrets_id_fk": { + "name": "secret_access_events_secret_id_company_secrets_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_issue_id_issues_id_fk": { + "name": "secret_access_events_issue_id_issues_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "secret_access_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_plugin_id_plugins_id_fk": { + "name": "secret_access_events_plugin_id_plugins_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.smoke_run_steps": { + "name": "smoke_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_step": { + "name": "scenario_step", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "screenshot_artifact_ref": { + "name": "screenshot_artifact_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "smoke_run_steps_company_run_idx": { + "name": "smoke_run_steps_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "smoke_run_steps_company_path_idx": { + "name": "smoke_run_steps_company_path_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "smoke_run_steps_company_id_companies_id_fk": { + "name": "smoke_run_steps_company_id_companies_id_fk", + "tableFrom": "smoke_run_steps", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "smoke_run_steps_run_id_smoke_runs_id_fk": { + "name": "smoke_run_steps_run_id_smoke_runs_id_fk", + "tableFrom": "smoke_run_steps", + "tableTo": "smoke_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.smoke_runs": { + "name": "smoke_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "smoke_runs_company_started_idx": { + "name": "smoke_runs_company_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "smoke_runs_company_status_idx": { + "name": "smoke_runs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "smoke_runs_company_id_companies_id_fk": { + "name": "smoke_runs_company_id_companies_id_fk", + "tableFrom": "smoke_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_card_updates": { + "name": "status_card_updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "card_id": { + "name": "card_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_issue_id": { + "name": "generation_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "query_version": { + "name": "query_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "status_card_updates_card_started_idx": { + "name": "status_card_updates_card_started_idx", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_card_updates_generation_issue_idx": { + "name": "status_card_updates_generation_issue_idx", + "columns": [ + { + "expression": "generation_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_card_updates_card_id_status_cards_id_fk": { + "name": "status_card_updates_card_id_status_cards_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "status_cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_card_updates_generation_issue_id_issues_id_fk": { + "name": "status_card_updates_generation_issue_id_issues_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "issues", + "columnsFrom": [ + "generation_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_card_updates_run_id_heartbeat_runs_id_fk": { + "name": "status_card_updates_run_id_heartbeat_runs_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_cards": { + "name": "status_cards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title_pinned": { + "name": "title_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "interest_prompt": { + "name": "interest_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queries": { + "name": "queries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "query_version": { + "name": "query_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "query_compiled_at": { + "name": "query_compiled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "query_compiled_by_agent_id": { + "name": "query_compiled_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_policy": { + "name": "refresh_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compiling'" + }, + "pending_change_count": { + "name": "pending_change_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pending_change_hash": { + "name": "pending_change_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_change_at": { + "name": "last_change_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "fingerprint_at": { + "name": "fingerprint_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "mentioned_issue_ids": { + "name": "mentioned_issue_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_update_run_kind": { + "name": "last_update_run_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_generated_at": { + "name": "last_generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "generating_issue_id": { + "name": "generating_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_eval_at": { + "name": "next_eval_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_by_user_id": { + "name": "archived_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_agent_id": { + "name": "archived_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "status_cards_company_archived_idx": { + "name": "status_cards_company_archived_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_cards_company_next_eval_idx": { + "name": "status_cards_company_next_eval_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_eval_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_cards_company_id_companies_id_fk": { + "name": "status_cards_company_id_companies_id_fk", + "tableFrom": "status_cards", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_cards_created_by_agent_id_agents_id_fk": { + "name": "status_cards_created_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_query_compiled_by_agent_id_agents_id_fk": { + "name": "status_cards_query_compiled_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "query_compiled_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_agent_id_agents_id_fk": { + "name": "status_cards_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_document_id_documents_id_fk": { + "name": "status_cards_document_id_documents_id_fk", + "tableFrom": "status_cards", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_generating_issue_id_issues_id_fk": { + "name": "status_cards_generating_issue_id_issues_id_fk", + "tableFrom": "status_cards", + "tableTo": "issues", + "columnsFrom": [ + "generating_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_archived_by_agent_id_agents_id_fk": { + "name": "status_cards_archived_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_decision_effects": { + "name": "status_decision_effects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "effect_kind": { + "name": "effect_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "status_decision_effects_decision_ordinal_uq": { + "name": "status_decision_effects_decision_ordinal_uq", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_decision_effects_company_idempotency_uq": { + "name": "status_decision_effects_company_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_decision_effects_company_id_companies_id_fk": { + "name": "status_decision_effects_company_id_companies_id_fk", + "tableFrom": "status_decision_effects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "status_decision_effects_issue_id_issues_id_fk": { + "name": "status_decision_effects_issue_id_issues_id_fk", + "tableFrom": "status_decision_effects", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "status_decision_effects_decision_id_status_decisions_id_fk": { + "name": "status_decision_effects_decision_id_status_decisions_id_fk", + "tableFrom": "status_decision_effects", + "tableTo": "status_decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_decisions": { + "name": "status_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assessment_id": { + "name": "assessment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision_version": { + "name": "decision_version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "policy_version": { + "name": "policy_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_status": { + "name": "from_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_status": { + "name": "to_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decision_json": { + "name": "decision_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_digest": { + "name": "decision_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_state": { + "name": "application_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'proposed'" + }, + "supersedes_decision_id": { + "name": "supersedes_decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "status_decisions_company_issue_version_uq": { + "name": "status_decisions_company_issue_version_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_decisions_company_assessment_uq": { + "name": "status_decisions_company_assessment_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assessment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_decisions_company_issue_digest_uq": { + "name": "status_decisions_company_issue_digest_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_decisions_company_id_companies_id_fk": { + "name": "status_decisions_company_id_companies_id_fk", + "tableFrom": "status_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "status_decisions_issue_id_issues_id_fk": { + "name": "status_decisions_issue_id_issues_id_fk", + "tableFrom": "status_decisions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "status_decisions_assessment_id_work_assessments_id_fk": { + "name": "status_decisions_assessment_id_work_assessments_id_fk", + "tableFrom": "status_decisions", + "tableTo": "work_assessments", + "columnsFrom": [ + "assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summary_slots": { + "name": "summary_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slot_key": { + "name": "slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "generating_issue_id": { + "name": "generating_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_generated_at": { + "name": "last_generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_generated_by_agent_id": { + "name": "last_generated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "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": { + "summary_slots_document_uq": { + "name": "summary_slots_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_scope_idx": { + "name": "summary_slots_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_generating_issue_idx": { + "name": "summary_slots_company_generating_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generating_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_updated_idx": { + "name": "summary_slots_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "summary_slots_company_id_companies_id_fk": { + "name": "summary_slots_company_id_companies_id_fk", + "tableFrom": "summary_slots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "summary_slots_document_id_documents_id_fk": { + "name": "summary_slots_document_id_documents_id_fk", + "tableFrom": "summary_slots", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "summary_slots_generating_issue_id_issues_id_fk": { + "name": "summary_slots_generating_issue_id_issues_id_fk", + "tableFrom": "summary_slots", + "tableTo": "issues", + "columnsFrom": [ + "generating_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "summary_slots_last_generated_by_agent_id_agents_id_fk": { + "name": "summary_slots_last_generated_by_agent_id_agents_id_fk", + "tableFrom": "summary_slots", + "tableTo": "agents", + "columnsFrom": [ + "last_generated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "summary_slots_company_scope_slot_uq": { + "name": "summary_slots_company_scope_slot_uq", + "nullsNotDistinct": true, + "columns": [ + "company_id", + "scope_kind", + "scope_id", + "slot_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_access_audit_events": { + "name": "tool_access_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "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": { + "tool_access_audit_company_created_idx": { + "name": "tool_access_audit_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_access_audit_connection_idx": { + "name": "tool_access_audit_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_access_audit_gateway_idx": { + "name": "tool_access_audit_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_access_audit_events_company_id_companies_id_fk": { + "name": "tool_access_audit_events_company_id_companies_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_access_audit_events_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_access_audit_events_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_access_audit_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_connection_id_tool_connections_id_fk": { + "name": "tool_access_audit_events_connection_id_tool_connections_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_access_audit_events_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_action_requests": { + "name": "tool_action_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_id": { + "name": "invocation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interaction_id": { + "name": "interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "canonical_arguments_hash": { + "name": "canonical_arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_arguments_summary": { + "name": "canonical_arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "signed_arguments": { + "name": "signed_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_markdown": { + "name": "preview_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_agent_id": { + "name": "decided_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_action_requests_company_status_idx": { + "name": "tool_action_requests_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_action_requests_invocation_idx": { + "name": "tool_action_requests_invocation_idx", + "columns": [ + { + "expression": "invocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_action_requests_issue_idx": { + "name": "tool_action_requests_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_action_requests_company_id_companies_id_fk": { + "name": "tool_action_requests_company_id_companies_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_action_requests_invocation_id_tool_invocations_id_fk": { + "name": "tool_action_requests_invocation_id_tool_invocations_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "tool_invocations", + "columnsFrom": [ + "invocation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_action_requests_issue_id_issues_id_fk": { + "name": "tool_action_requests_issue_id_issues_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_interaction_id_issue_thread_interactions_id_fk": { + "name": "tool_action_requests_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "issue_thread_interactions", + "columnsFrom": [ + "interaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_approval_id_approvals_id_fk": { + "name": "tool_action_requests_approval_id_approvals_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_requested_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_requested_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_resolved_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_resolved_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_decided_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_decided_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "decided_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_applications": { + "name": "tool_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_key": { + "name": "application_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_applications_company_idx": { + "name": "tool_applications_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_status_idx": { + "name": "tool_applications_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_name_uq": { + "name": "tool_applications_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_key_uq": { + "name": "tool_applications_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "application_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_applications_company_id_companies_id_fk": { + "name": "tool_applications_company_id_companies_id_fk", + "tableFrom": "tool_applications", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_applications_plugin_id_plugins_id_fk": { + "name": "tool_applications_plugin_id_plugins_id_fk", + "tableFrom": "tool_applications", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_applications_owner_agent_id_agents_id_fk": { + "name": "tool_applications_owner_agent_id_agents_id_fk", + "tableFrom": "tool_applications", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_call_events": { + "name": "tool_call_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invocation_id": { + "name": "invocation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action_request_id": { + "name": "action_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_slot_id": { + "name": "runtime_slot_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_policy_ids": { + "name": "matched_policy_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy_explanation": { + "name": "policy_explanation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_scope_summary": { + "name": "credential_scope_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "header_policy_summary": { + "name": "header_policy_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "arguments_summary": { + "name": "arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_summary": { + "name": "request_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_size_bytes": { + "name": "result_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "redaction_plan": { + "name": "redaction_plan", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rate_limit_state": { + "name": "rate_limit_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_call_events_company_created_idx": { + "name": "tool_call_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_run_idx": { + "name": "tool_call_events_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_issue_idx": { + "name": "tool_call_events_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_invocation_idx": { + "name": "tool_call_events_invocation_idx", + "columns": [ + { + "expression": "invocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_gateway_idx": { + "name": "tool_call_events_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_call_events_company_id_companies_id_fk": { + "name": "tool_call_events_company_id_companies_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_call_events_agent_id_agents_id_fk": { + "name": "tool_call_events_agent_id_agents_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_run_id_heartbeat_runs_id_fk": { + "name": "tool_call_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_issue_id_issues_id_fk": { + "name": "tool_call_events_issue_id_issues_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_call_events_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_call_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_application_id_tool_applications_id_fk": { + "name": "tool_call_events_application_id_tool_applications_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_connection_id_tool_connections_id_fk": { + "name": "tool_call_events_connection_id_tool_connections_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_call_events_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_invocation_id_tool_invocations_id_fk": { + "name": "tool_call_events_invocation_id_tool_invocations_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_invocations", + "columnsFrom": [ + "invocation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_action_request_id_tool_action_requests_id_fk": { + "name": "tool_call_events_action_request_id_tool_action_requests_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_action_requests", + "columnsFrom": [ + "action_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_runtime_slot_id_tool_runtime_slots_id_fk": { + "name": "tool_call_events_runtime_slot_id_tool_runtime_slots_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_runtime_slots", + "columnsFrom": [ + "runtime_slot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_catalog_entries": { + "name": "tool_catalog_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'tool'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "output_schema": { + "name": "output_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read'" + }, + "is_read_only": { + "name": "is_read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_write": { + "name": "is_write", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_destructive": { + "name": "is_destructive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version_hash": { + "name": "version_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_agent_id": { + "name": "reviewed_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_user_id": { + "name": "reviewed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quarantined_at": { + "name": "quarantined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "quarantine_reason": { + "name": "quarantine_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_catalog_entries_company_idx": { + "name": "tool_catalog_entries_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_application_idx": { + "name": "tool_catalog_entries_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_connection_idx": { + "name": "tool_catalog_entries_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_company_status_idx": { + "name": "tool_catalog_entries_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_connection_name_uq": { + "name": "tool_catalog_entries_connection_name_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_catalog_entries_company_id_companies_id_fk": { + "name": "tool_catalog_entries_company_id_companies_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_application_id_tool_applications_id_fk": { + "name": "tool_catalog_entries_application_id_tool_applications_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_connection_id_tool_connections_id_fk": { + "name": "tool_catalog_entries_connection_id_tool_connections_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_reviewed_by_agent_id_agents_id_fk": { + "name": "tool_catalog_entries_reviewed_by_agent_id_agents_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "agents", + "columnsFrom": [ + "reviewed_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_connection_installs": { + "name": "tool_connection_installs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_connection_installs_company_target_idx": { + "name": "tool_connection_installs_company_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connection_installs_connection_idx": { + "name": "tool_connection_installs_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connection_installs_target_uq": { + "name": "tool_connection_installs_target_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_connection_installs_company_id_companies_id_fk": { + "name": "tool_connection_installs_company_id_companies_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connection_installs_connection_id_tool_connections_id_fk": { + "name": "tool_connection_installs_connection_id_tool_connections_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connection_installs_created_by_agent_id_agents_id_fk": { + "name": "tool_connection_installs_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "tool_connection_installs_target_type_check": { + "name": "tool_connection_installs_target_type_check", + "value": "\"tool_connection_installs\".\"target_type\" in ('company', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.tool_connections": { + "name": "tool_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'managed'" + }, + "ownership": { + "name": "ownership", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'customer'" + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "transport_config": { + "name": "transport_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "credential_refs": { + "name": "credential_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "credential_secret_refs": { + "name": "credential_secret_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unchecked'" + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_checked_at": { + "name": "health_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_health_at": { + "name": "last_health_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_catalog_refresh_at": { + "name": "last_catalog_refresh_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_connections_company_idx": { + "name": "tool_connections_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_application_idx": { + "name": "tool_connections_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_company_enabled_idx": { + "name": "tool_connections_company_enabled_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_company_uid_uq": { + "name": "tool_connections_company_uid_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_connections_company_id_companies_id_fk": { + "name": "tool_connections_company_id_companies_id_fk", + "tableFrom": "tool_connections", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connections_application_id_tool_applications_id_fk": { + "name": "tool_connections_application_id_tool_applications_id_fk", + "tableFrom": "tool_connections", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tool_connections_created_by_agent_id_agents_id_fk": { + "name": "tool_connections_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_connections", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tool_connections_company_id_uq": { + "name": "tool_connections_company_id_uq", + "nullsNotDistinct": false, + "columns": [ + "company_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "tool_connections_ownership_check": { + "name": "tool_connections_ownership_check", + "value": "\"tool_connections\".\"ownership\" in ('platform_shared', 'platform_provisioned', 'customer', 'dcr')" + }, + "tool_connections_transport_check": { + "name": "tool_connections_transport_check", + "value": "\"tool_connections\".\"transport\" in ('mcp_remote', 'rest_api', 'local_stdio')" + }, + "tool_connections_auth_kind_check": { + "name": "tool_connections_auth_kind_check", + "value": "\"tool_connections\".\"auth_kind\" in ('oauth', 'api_key', 'none')" + } + }, + "isRLSEnabled": false + }, + "public.tool_gateway_rate_limit_counters": { + "name": "tool_gateway_rate_limit_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "counter_key": { + "name": "counter_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start_at": { + "name": "window_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_ms": { + "name": "window_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_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": { + "tool_gateway_rate_limit_counters_company_idx": { + "name": "tool_gateway_rate_limit_counters_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_rate_limit_counters_window_uq": { + "name": "tool_gateway_rate_limit_counters_window_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "counter_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_gateway_rate_limit_counters_company_id_companies_id_fk": { + "name": "tool_gateway_rate_limit_counters_company_id_companies_id_fk", + "tableFrom": "tool_gateway_rate_limit_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_gateway_sessions": { + "name": "tool_gateway_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_gateway_sessions_token_hash_uq": { + "name": "tool_gateway_sessions_token_hash_uq", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_company_agent_idx": { + "name": "tool_gateway_sessions_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_company_expires_idx": { + "name": "tool_gateway_sessions_company_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_run_idx": { + "name": "tool_gateway_sessions_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_issue_idx": { + "name": "tool_gateway_sessions_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_gateway_idx": { + "name": "tool_gateway_sessions_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_gateway_sessions_company_id_companies_id_fk": { + "name": "tool_gateway_sessions_company_id_companies_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_agent_id_agents_id_fk": { + "name": "tool_gateway_sessions_agent_id_agents_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_run_id_heartbeat_runs_id_fk": { + "name": "tool_gateway_sessions_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_issue_id_issues_id_fk": { + "name": "tool_gateway_sessions_issue_id_issues_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_project_id_projects_id_fk": { + "name": "tool_gateway_sessions_project_id_projects_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_gateway_sessions_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_gateway_sessions_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_invocations": { + "name": "tool_invocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_version_hash": { + "name": "catalog_version_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "catalog_schema_hash": { + "name": "catalog_schema_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_key": { + "name": "application_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream_tool_name": { + "name": "upstream_tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "arguments_hash": { + "name": "arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "arguments_summary": { + "name": "arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "policy_decision": { + "name": "policy_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_policy_ids": { + "name": "matched_policy_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "policy_explanation": { + "name": "policy_explanation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_scope_summary": { + "name": "credential_scope_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "header_policy_summary": { + "name": "header_policy_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_required'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "upstream_request_id": { + "name": "upstream_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_size_bytes": { + "name": "result_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "result_artifact_id": { + "name": "result_artifact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_invocations_company_created_idx": { + "name": "tool_invocations_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_run_idx": { + "name": "tool_invocations_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_issue_idx": { + "name": "tool_invocations_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_gateway_idx": { + "name": "tool_invocations_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_company_idempotency_uq": { + "name": "tool_invocations_company_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_invocations_company_id_companies_id_fk": { + "name": "tool_invocations_company_id_companies_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_invocations_agent_id_agents_id_fk": { + "name": "tool_invocations_agent_id_agents_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_issue_id_issues_id_fk": { + "name": "tool_invocations_issue_id_issues_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_run_id_heartbeat_runs_id_fk": { + "name": "tool_invocations_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_invocations_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_invocations_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_application_id_tool_applications_id_fk": { + "name": "tool_invocations_application_id_tool_applications_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_connection_id_tool_connections_id_fk": { + "name": "tool_invocations_connection_id_tool_connections_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_invocations_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_mcp_gateway_tokens": { + "name": "tool_mcp_gateway_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gateway_client'" + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_label": { + "name": "client_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "owner_note": { + "name": "owner_note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "allowed_actions": { + "name": "allowed_actions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"tools/list\",\"tools/call\"]'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expiry_override_reason": { + "name": "expiry_override_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_override_by_user_id": { + "name": "expiry_override_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_override_by_agent_id": { + "name": "expiry_override_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expiry_override_at": { + "name": "expiry_override_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 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_mcp_gateway_tokens_token_hash_uq": { + "name": "tool_mcp_gateway_tokens_token_hash_uq", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_gateway_idx": { + "name": "tool_mcp_gateway_tokens_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_subject_idx": { + "name": "tool_mcp_gateway_tokens_subject_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_company_expires_idx": { + "name": "tool_mcp_gateway_tokens_company_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_mcp_gateway_tokens_company_id_companies_id_fk": { + "name": "tool_mcp_gateway_tokens_company_id_companies_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_mcp_gateway_tokens_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_expiry_override_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateway_tokens_expiry_override_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "agents", + "columnsFrom": [ + "expiry_override_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_created_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateway_tokens_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_mcp_gateways": { + "name": "tool_mcp_gateways", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gw_' || replace(gen_random_uuid()::text, '-', '')" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_slug": { + "name": "display_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "default_profile_mode": { + "name": "default_profile_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gateway_only'" + }, + "context_scope_type": { + "name": "context_scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "context_scope_id": { + "name": "context_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_issue_id": { + "name": "approval_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auth_config": { + "name": "auth_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"bearer\":{\"enabled\":true,\"tokenPrefix\":\"pcgw\",\"defaultTtlSeconds\":7776000,\"requireFiniteExpiry\":true,\"longLivedTokenRequiresOverride\":true},\"oauth\":{\"enabled\":false,\"reservedFor\":\"v1_5\",\"dynamicClientRegistration\":false,\"authorizationCodePkce\":false}}'::jsonb" + }, + "header_policy": { + "name": "header_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"callerPassthrough\":{\"enabled\":false,\"allowedHeaders\":[]},\"staticHeaders\":[],\"generatedMetadata\":{\"enabled\":false,\"allowedHeaders\":[]},\"responseHeaders\":{\"forwardMcpRequiredHeaders\":true,\"forwardSafeCacheHeaders\":true}}'::jsonb" + }, + "metadata_policy": { + "name": "metadata_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"forwardCompanyId\":false,\"forwardGatewayId\":false,\"forwardProjectId\":false,\"forwardIssueId\":false,\"forwardAgentId\":false,\"forwardRunId\":false,\"forwardCorrelationId\":true}'::jsonb" + }, + "on_demand_tools_config": { + "name": "on_demand_tools_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"enabled\":false,\"searchToolName\":\"search_tools\",\"runToolName\":\"run_tool\"}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_mcp_gateways_company_idx": { + "name": "tool_mcp_gateways_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_status_idx": { + "name": "tool_mcp_gateways_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_profile_idx": { + "name": "tool_mcp_gateways_profile_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_public_id_uq": { + "name": "tool_mcp_gateways_public_id_uq", + "columns": [ + { + "expression": "gateway_public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_slug_uq": { + "name": "tool_mcp_gateways_company_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_name_uq": { + "name": "tool_mcp_gateways_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_mcp_gateways_company_id_companies_id_fk": { + "name": "tool_mcp_gateways_company_id_companies_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateways_profile_id_tool_profiles_id_fk": { + "name": "tool_mcp_gateways_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "tool_mcp_gateways_agent_id_agents_id_fk": { + "name": "tool_mcp_gateways_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_project_id_projects_id_fk": { + "name": "tool_mcp_gateways_project_id_projects_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_issue_id_issues_id_fk": { + "name": "tool_mcp_gateways_issue_id_issues_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_approval_issue_id_issues_id_fk": { + "name": "tool_mcp_gateways_approval_issue_id_issues_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "issues", + "columnsFrom": [ + "approval_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_created_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateways_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_oauth_states": { + "name": "tool_oauth_states", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_session_id": { + "name": "created_by_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_scopes": { + "name": "requested_scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interaction_id": { + "name": "interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": { + "tool_oauth_states_company_idx": { + "name": "tool_oauth_states_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_connection_idx": { + "name": "tool_oauth_states_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_actor_idx": { + "name": "tool_oauth_states_actor_idx", + "columns": [ + { + "expression": "created_by_actor_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_expires_at_idx": { + "name": "tool_oauth_states_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_oauth_states_company_id_companies_id_fk": { + "name": "tool_oauth_states_company_id_companies_id_fk", + "tableFrom": "tool_oauth_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_oauth_states_connection_id_tool_connections_id_fk": { + "name": "tool_oauth_states_connection_id_tool_connections_id_fk", + "tableFrom": "tool_oauth_states", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policies": { + "name": "tool_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy_type": { + "name": "policy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "selectors": { + "name": "selectors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_policies_company_enabled_idx": { + "name": "tool_policies_company_enabled_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_policies_company_type_idx": { + "name": "tool_policies_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "policy_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_policies_company_name_uq": { + "name": "tool_policies_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_policies_company_id_companies_id_fk": { + "name": "tool_policies_company_id_companies_id_fk", + "tableFrom": "tool_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_policies_created_by_agent_id_agents_id_fk": { + "name": "tool_policies_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_policies", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profile_bindings": { + "name": "tool_profile_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_profile_bindings_company_target_idx": { + "name": "tool_profile_bindings_company_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_bindings_target_profile_uq": { + "name": "tool_profile_bindings_target_profile_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profile_bindings_company_id_companies_id_fk": { + "name": "tool_profile_bindings_company_id_companies_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_bindings_profile_id_tool_profiles_id_fk": { + "name": "tool_profile_bindings_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_bindings_created_by_agent_id_agents_id_fk": { + "name": "tool_profile_bindings_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profile_entries": { + "name": "tool_profile_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selector_type": { + "name": "selector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'include'" + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_profile_entries_company_profile_idx": { + "name": "tool_profile_entries_company_profile_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_application_idx": { + "name": "tool_profile_entries_application_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_connection_idx": { + "name": "tool_profile_entries_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_catalog_entry_idx": { + "name": "tool_profile_entries_catalog_entry_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profile_entries_company_id_companies_id_fk": { + "name": "tool_profile_entries_company_id_companies_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_profile_id_tool_profiles_id_fk": { + "name": "tool_profile_entries_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_application_id_tool_applications_id_fk": { + "name": "tool_profile_entries_application_id_tool_applications_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_connection_id_tool_connections_id_fk": { + "name": "tool_profile_entries_connection_id_tool_connections_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_profile_entries_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profiles": { + "name": "tool_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_key": { + "name": "profile_key", + "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'" + }, + "default_action": { + "name": "default_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'deny'" + }, + "new_tools_reviewed_at": { + "name": "new_tools_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_profiles_company_status_idx": { + "name": "tool_profiles_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profiles_company_key_uq": { + "name": "tool_profiles_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profiles_company_name_uq": { + "name": "tool_profiles_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profiles_company_id_companies_id_fk": { + "name": "tool_profiles_company_id_companies_id_fk", + "tableFrom": "tool_profiles", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_rate_limit_counters": { + "name": "tool_rate_limit_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "counter_key": { + "name": "counter_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start_at": { + "name": "window_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reset_at": { + "name": "reset_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": { + "tool_rate_limit_counters_company_idx": { + "name": "tool_rate_limit_counters_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_rate_limit_counters_window_uq": { + "name": "tool_rate_limit_counters_window_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "counter_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_rate_limit_counters_company_id_companies_id_fk": { + "name": "tool_rate_limit_counters_company_id_companies_id_fk", + "tableFrom": "tool_rate_limit_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_rate_limit_counters_policy_id_tool_policies_id_fk": { + "name": "tool_rate_limit_counters_policy_id_tool_policies_id_fk", + "tableFrom": "tool_rate_limit_counters", + "tableTo": "tool_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_runtime_metric_counters": { + "name": "tool_runtime_metric_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket_start_at": { + "name": "bucket_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_runtime_metric_counters_company_metric_idx": { + "name": "tool_runtime_metric_counters_company_metric_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_metric_counters_bucket_uq": { + "name": "tool_runtime_metric_counters_bucket_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_runtime_metric_counters_company_id_companies_id_fk": { + "name": "tool_runtime_metric_counters_company_id_companies_id_fk", + "tableFrom": "tool_runtime_metric_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_runtime_slots": { + "name": "tool_runtime_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_scope_type": { + "name": "owner_scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connection'" + }, + "owner_scope_id": { + "name": "owner_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_kind": { + "name": "runtime_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_stdio'" + }, + "slot_key": { + "name": "slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stopped'" + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_scope": { + "name": "workspace_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_scope_hash": { + "name": "credential_scope_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_id": { + "name": "process_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "command_template_key": { + "name": "command_template_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unchecked'" + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_health_check_at": { + "name": "last_health_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stopped_at": { + "name": "stopped_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 + }, + "idle_expires_at": { + "name": "idle_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "idle_deadline_at": { + "name": "idle_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_runtime_slots_company_idx": { + "name": "tool_runtime_slots_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_connection_idx": { + "name": "tool_runtime_slots_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_execution_workspace_idx": { + "name": "tool_runtime_slots_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_slot_key_uq": { + "name": "tool_runtime_slots_slot_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slot_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_runtime_slots_company_id_companies_id_fk": { + "name": "tool_runtime_slots_company_id_companies_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_runtime_slots_application_id_tool_applications_id_fk": { + "name": "tool_runtime_slots_application_id_tool_applications_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_connection_id_tool_connections_id_fk": { + "name": "tool_runtime_slots_connection_id_tool_connections_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_runtime_slots_project_workspace_id_project_workspaces_id_fk": { + "name": "tool_runtime_slots_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_execution_workspace_id_execution_workspaces_id_fk": { + "name": "tool_runtime_slots_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_issue_id_issues_id_fk": { + "name": "tool_runtime_slots_issue_id_issues_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_stdio_command_templates": { + "name": "tool_stdio_command_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "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'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "env_keys": { + "name": "env_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_stdio_command_templates_company_idx": { + "name": "tool_stdio_command_templates_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_stdio_command_templates_company_status_idx": { + "name": "tool_stdio_command_templates_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_stdio_command_templates_company_key_uq": { + "name": "tool_stdio_command_templates_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_stdio_command_templates_company_id_companies_id_fk": { + "name": "tool_stdio_command_templates_company_id_companies_id_fk", + "tableFrom": "tool_stdio_command_templates", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_stdio_command_templates_created_by_agent_id_agents_id_fk": { + "name": "tool_stdio_command_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_stdio_command_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_inbox_agent_policies": { + "name": "user_inbox_agent_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "allowed_agent_ids": { + "name": "allowed_agent_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_inbox_agent_policies_company_user_uq": { + "name": "user_inbox_agent_policies_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_inbox_agent_policies_allowed_agent_ids_idx": { + "name": "user_inbox_agent_policies_allowed_agent_ids_idx", + "columns": [ + { + "expression": "allowed_agent_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "user_inbox_agent_policies_company_id_companies_id_fk": { + "name": "user_inbox_agent_policies_company_id_companies_id_fk", + "tableFrom": "user_inbox_agent_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_inbox_agent_policies_mode_check": { + "name": "user_inbox_agent_policies_mode_check", + "value": "\"user_inbox_agent_policies\".\"mode\" in ('open', 'allowlist', 'disabled')" + } + }, + "isRLSEnabled": false + }, + "public.user_secret_declarations": { + "name": "user_secret_declarations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_selector": { + "name": "version_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'latest'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_missing_override": { + "name": "allow_missing_override", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "label": { + "name": "label", + "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": { + "user_secret_declarations_company_idx": { + "name": "user_secret_declarations_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_definition_idx": { + "name": "user_secret_declarations_definition_idx", + "columns": [ + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_target_idx": { + "name": "user_secret_declarations_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_company_required_idx": { + "name": "user_secret_declarations_company_required_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "required", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_target_path_uq": { + "name": "user_secret_declarations_target_path_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_required_override_idx": { + "name": "user_secret_declarations_required_override_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "allow_missing_override", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_secret_declarations\".\"allow_missing_override\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_secret_declarations_company_id_companies_id_fk": { + "name": "user_secret_declarations_company_id_companies_id_fk", + "tableFrom": "user_secret_declarations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "user_secret_declarations", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_secret_definitions": { + "name": "user_secret_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "managed_mode": { + "name": "managed_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip_managed'" + }, + "provider_config_id": { + "name": "provider_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_guidance": { + "name": "usage_guidance", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_secret_definitions_company_status_idx": { + "name": "user_secret_definitions_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_company_provider_idx": { + "name": "user_secret_definitions_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_provider_config_idx": { + "name": "user_secret_definitions_provider_config_idx", + "columns": [ + { + "expression": "provider_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_company_key_uq": { + "name": "user_secret_definitions_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_secret_definitions\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_secret_definitions_company_id_companies_id_fk": { + "name": "user_secret_definitions_company_id_companies_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk": { + "name": "user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "company_secret_provider_configs", + "columnsFrom": [ + "provider_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_secret_definitions_created_by_agent_id_agents_id_fk": { + "name": "user_secret_definitions_created_by_agent_id_agents_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_secret_definitions_updated_by_agent_id_agents_id_fk": { + "name": "user_secret_definitions_updated_by_agent_id_agents_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sidebar_preferences": { + "name": "user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_order": { + "name": "company_order", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_sidebar_preferences_user_uq": { + "name": "user_sidebar_preferences_user_uq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.work_assessments": { + "name": "work_assessments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "result_id": { + "name": "result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_kind": { + "name": "trigger_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_ref": { + "name": "trigger_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_capability": { + "name": "trigger_capability", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_actor_company_id": { + "name": "trigger_actor_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prior_issue_status": { + "name": "prior_issue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prior_status_version": { + "name": "prior_status_version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "prior_decision_id": { + "name": "prior_decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "policy_version": { + "name": "policy_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assessment_json": { + "name": "assessment_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_digest": { + "name": "input_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "supersedes_assessment_id": { + "name": "supersedes_assessment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "work_assessments_company_issue_input_uq": { + "name": "work_assessments_company_issue_input_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "input_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "work_assessments_company_id_companies_id_fk": { + "name": "work_assessments_company_id_companies_id_fk", + "tableFrom": "work_assessments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "work_assessments_issue_id_issues_id_fk": { + "name": "work_assessments_issue_id_issues_id_fk", + "tableFrom": "work_assessments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "work_assessments_run_id_heartbeat_runs_id_fk": { + "name": "work_assessments_run_id_heartbeat_runs_id_fk", + "tableFrom": "work_assessments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "work_assessments_contract_id_completion_contracts_id_fk": { + "name": "work_assessments_contract_id_completion_contracts_id_fk", + "tableFrom": "work_assessments", + "tableTo": "completion_contracts", + "columnsFrom": [ + "contract_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "work_assessments_result_id_native_run_results_id_fk": { + "name": "work_assessments_result_id_native_run_results_id_fk", + "tableFrom": "work_assessments", + "tableTo": "native_run_results", + "columnsFrom": [ + "result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "work_assessments_trigger_actor_company_id_companies_id_fk": { + "name": "work_assessments_trigger_actor_company_id_companies_id_fk", + "tableFrom": "work_assessments", + "tableTo": "companies", + "columnsFrom": [ + "trigger_actor_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_issue_started_idx": { + "name": "workspace_operations_company_workspace_issue_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_issue_id_issues_id_fk": { + "name": "workspace_operations_issue_id_issues_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exposure_handle": { + "name": "exposure_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backend_url": { + "name": "backend_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 4c93bc12852..6ecbad636e7 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1590,6 +1590,13 @@ "when": 1787359498156, "tag": "0228_late_landau", "breakpoints": true + }, + { + "idx": 229, + "version": "7", + "when": 1787416987132, + "tag": "0229_pink_longshot", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 9f3c54f1084..c1cfe37491f 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -111,6 +111,7 @@ export { documentAnnotationComments } from "./document_annotation_comments.js"; export { documentAnnotationAnchorSnapshots } from "./document_annotation_anchor_snapshots.js"; export { heartbeatRuns } from "./heartbeat_runs.js"; export { heartbeatRunEvents } from "./heartbeat_run_events.js"; +export { providerTraceRecords } from "./provider_trace_records.js"; export { completionContracts } from "./completion_contracts.js"; export { nativeRunResults } from "./native_run_results.js"; export { nativeRunFinalizations } from "./native_run_finalizations.js"; diff --git a/packages/db/src/schema/provider_trace_records.ts b/packages/db/src/schema/provider_trace_records.ts new file mode 100644 index 00000000000..607fd1eb5a6 --- /dev/null +++ b/packages/db/src/schema/provider_trace_records.ts @@ -0,0 +1,53 @@ +import { + bigint, + index, + integer, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { heartbeatRuns } from "./heartbeat_runs.js"; + +/** Metadata only. Exact provider bytes live in the restricted sidecar store. */ +export const providerTraceRecords = pgTable( + "provider_trace_records", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id") + .notNull() + .references(() => companies.id), + runId: uuid("run_id") + .notNull() + .references(() => heartbeatRuns.id, { onDelete: "cascade" }), + status: text("status").notNull().default("capturing"), + provider: text("provider").notNull(), + traceRef: text("trace_ref").notNull(), + frameCount: integer("frame_count").notNull().default(0), + byteCount: bigint("byte_count", { mode: "number" }).notNull().default(0), + digest: text("digest"), + reason: text("reason"), + requestedBy: text("requested_by").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + deletedAt: timestamp("deleted_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => ({ + runUnique: uniqueIndex("provider_trace_records_run_unique").on(table.runId), + expiryIdx: index("provider_trace_records_expiry_idx").on( + table.status, + table.expiresAt, + ), + companyCreatedIdx: index("provider_trace_records_company_created_idx").on( + table.companyId, + table.createdAt, + ), + }), +); diff --git a/packages/paperclip-runner/devtools/issue-thread/src/App.tsx b/packages/paperclip-runner/devtools/issue-thread/src/App.tsx index f0eaf078a4d..a5f919560dc 100644 --- a/packages/paperclip-runner/devtools/issue-thread/src/App.tsx +++ b/packages/paperclip-runner/devtools/issue-thread/src/App.tsx @@ -89,8 +89,15 @@ interface EmbeddedEvalReport { run: { model: string; provider: string; + driver: string; + providerVersion: string | null; + runnerProvider: string; configuration: string; sessionId: string; + providerSessionId: string | null; + agentVersion: string | null; + retainedSession: boolean | null; + retainedSessionStatus: string | null; fixtureDigest: string; runnerPackageDigest: string; runnerdDigest: string; @@ -107,6 +114,7 @@ interface EmbeddedEvalReport { outputTokens: number; cachedInputTokens: number; reasoningTokens: number; + providerReportedCostNanodollars?: number; estimatedCostNanodollars: number; pricingVersion: string; } | null; @@ -411,7 +419,21 @@ export function App() { if (cancelled) return; rememberSession(response.sessionId, "cleanroom"); setIdentity(response.identity ?? null); - setActiveHarness(response.configuration ?? initialHarnessRef.current); + const configuration = response.configuration ?? initialHarnessRef.current; + setActiveHarness(configuration); + // The server-returned immutable session configuration is authoritative + // for both the active status and the options initially shown for the + // next chat. Without this, a restored remote session could say Claude + // or AgentCore in the header while leaving Codex-only fields in the + // options form. + setHarness(configuration); + setManagedBudgetInput(String( + configuration.provider === "aws_agentcore" + ? configuration.maxEstimatedSessionCostUsd ?? 1 + : configuration.provider === "claude_managed" + ? configuration.maxSessionListCostUsd ?? 1 + : 1, + )); setProviderRuntime(response.runtime); setHistoricSessionId(null); setSnapshot(response.view); @@ -758,11 +780,14 @@ export function App() { const adoptCleanRoom = useCallback((next: Awaited>) => { rememberSession(next.sessionId, "cleanroom"); setIdentity(next.identity ?? null); - setActiveHarness(next.configuration ?? harness); + const configuration = next.configuration ?? harness; + setActiveHarness(configuration); + setHarness(configuration); setProviderRuntime(next.runtime); setHistoricSessionId(null); setSnapshot(next.view); setDevtools(null); + setError(null); setActionError(null); setAnnouncement( `New clean-room chat started on ${next.view.issue.identifier}. The previous session was closed.`, @@ -870,6 +895,7 @@ export function App() { setSnapshot(live.snapshot); setIdentity(live.identity); setActiveHarness(live.configuration ?? null); + if (live.configuration !== undefined) setHarness(live.configuration); setProviderRuntime(live.runtime); setDevtools(null); setHistoricSessionId(null); @@ -886,6 +912,7 @@ export function App() { setSnapshot(restored.view); setIdentity(restored.identity ?? archived.identity); setActiveHarness(restored.configuration ?? configuration); + setHarness(restored.configuration ?? configuration); setProviderRuntime(restored.runtime); setDevtools(null); setHistoricSessionId(null); @@ -1020,10 +1047,141 @@ export function App() { : error}

{chat ? ( -

- The clean room only runs against the selected real provider through real runnerd, so it does not fall - back to a fixture or a recording. -

+ <> +

+ The clean room only runs against the selected real provider through real runnerd, so it does not fall + back to a fixture or a recording. +

+
+ + {harness.provider === "acpx" ? ( + + ) : null} + + {harness.lifecyclePolicy.mode === "warm" ? ( + + ) : null} + {harness.provider === "aws_agentcore" ? ( + <> + + + + ) : null} + + +
+ ) : null} - - + + State monitor + + setQuery(event.target.value)} + placeholder="operation, entity, turn…" + /> + + + -
- {([...(evalReport ? [{ id: "eval" as const, icon: "evidence" as const, label: "Eval" }] : []), ...TABS]).map(({ id, icon, label }) => ( - +
+ {[ + ...(evalReport + ? [ + { + id: "eval" as const, + icon: "evidence" as const, + label: "Eval", + }, + ] + : []), + ...TABS, + ].map(({ id, icon, label }) => ( + ))}
{tab === "eval" && evalReport ? (
-
← Eval suite{evalReport.passed ? "PASS" : evalReport.disposition.replaceAll("_", " ").toUpperCase()}{evalReport.attemptId}
+
+ ← Eval suite + + {evalReport.passed + ? "PASS" + : evalReport.disposition.replaceAll("_", " ").toUpperCase()} + + {evalReport.attemptId} +
-
Model
{evalReport.run.model.startsWith(`${evalReport.run.provider}/`) ? evalReport.run.model : `${evalReport.run.provider}/${evalReport.run.model}`}
-
Configuration
{evalReport.run.configuration}
-
Session
{evalReport.run.sessionId}
-
Duration
{evalReport.run.durationMs} ms
-
Fixture
{evalReport.run.fixtureDigest}
-
State
r{evalReport.run.initialRevision} → r{evalReport.run.finalRevision}
-
Tokens
{evalReport.run.usage === null ? "unknown" : `${evalReport.run.usage.inputTokens} in · ${evalReport.run.usage.outputTokens} out · ${evalReport.run.usage.cachedInputTokens} cached`}
-
Agent turns
{evalReport.run.usage?.agentTurns ?? "unknown"}
-
Provider requests
{evalReport.run.usage?.providerRequests ?? "unavailable"}
-
Estimated cost
{evalReport.run.usage === null ? "unknown" : `$${(evalReport.run.usage.estimatedCostNanodollars / 1_000_000_000).toFixed(6)} · ${evalReport.run.usage.pricingVersion}`}
-
Runner
{evalReport.run.runnerPackageDigest}
-
Runner build
{evalReport.run.runnerBuild}
-
runnerd
{evalReport.run.runnerdDigest}
+
+
Model
+
+ {evalReport.run.model.startsWith(`${evalReport.run.provider}/`) + ? evalReport.run.model + : `${evalReport.run.provider}/${evalReport.run.model}`} +
+
+
+
Configuration
+
{evalReport.run.configuration}
+
+
+
Session
+
{evalReport.run.sessionId}
+
+
+
Provider session
+
{evalReport.run.providerSessionId ?? "unavailable"}
+
+
+
Driver
+
+ {evalReport.run.driver} + {evalReport.run.providerVersion + ? ` · ${evalReport.run.providerVersion}` + : ""} +
+
+ {evalReport.run.agentVersion ? ( +
+
Agent version
+
{evalReport.run.agentVersion}
+
+ ) : null} +
+
Retained session
+
+ {evalReport.run.retainedSession === true + ? (evalReport.run.retainedSessionStatus ?? "retained") + : "not applicable"} +
+
+
+
Duration
+
{evalReport.run.durationMs} ms
+
+
+
Fixture
+
{evalReport.run.fixtureDigest}
+
+
+
State
+
+ r{evalReport.run.initialRevision} → r + {evalReport.run.finalRevision} +
+
+
+
Tokens
+
+ {evalReport.run.usage === null + ? "unknown" + : `${evalReport.run.usage.inputTokens} in · ${evalReport.run.usage.outputTokens} out · ${evalReport.run.usage.cachedInputTokens} cached`} +
+
+
+
Agent turns
+
{evalReport.run.usage?.agentTurns ?? "unknown"}
+
+
+
Provider requests
+
{evalReport.run.usage?.providerRequests ?? "unavailable"}
+
+
+
Estimated cost
+
+ {evalReport.run.usage === null + ? "unknown" + : `$${(evalReport.run.usage.estimatedCostNanodollars / 1_000_000_000).toFixed(6)} · ${evalReport.run.usage.pricingVersion}`} +
+
+
+
Provider list cost
+
+ {typeof evalReport.run.usage + ?.providerReportedCostNanodollars !== "number" + ? "unknown" + : `$${(evalReport.run.usage.providerReportedCostNanodollars / 1_000_000_000).toFixed(6)}`} +
+
+
+
Runner
+
{evalReport.run.runnerPackageDigest}
+
+
+
Runner build
+
{evalReport.run.runnerBuild}
+
+
+
runnerd
+
{evalReport.run.runnerdDigest}
+
-

Assertions

+

Assertions

+
) : null} {tab === "timeline" ? (
{revisions.map((entry) => ( - ))}
) : null} {tab === "state" ? (
- - setRevision(Number(event.target.value))} + > + {snapshot.revisions.map((entry) => ( + + ))}
@@ -180,36 +490,157 @@ export function DevtoolsInspector({ snapshot, onFork, tab, onTabChange, evalRepo {tab === "diff" ? (
- + setCompareRevision(Number(event.target.value)) + } + > + {snapshot.revisions.map((entry) => ( + + ))} - setRevision(Number(event.target.value))} + > + {snapshot.revisions.map((entry) => ( + + ))}
- {rows.map((row) =>
{row.path}{JSON.stringify(row.before)}{JSON.stringify(row.after)}
)} - {rows.length === 0 ?

No changes between these revisions.

: null} + {rows.map((row) => ( +
+ {row.path} + {JSON.stringify(row.before)} + {JSON.stringify(row.after)} +
+ ))} + {rows.length === 0 ? ( +

No changes between these revisions.

+ ) : null}
) : null} {tab === "documents" ? ( - documents.length === 0 ?

No documents exist at r{selected.revision}.

: ( + documents.length === 0 ? ( +
+ +

No documents exist at r{selected.revision}.

+
+ ) : (
- {documents.map((entry) => )} + {documents.map((entry) => ( + + ))}
{document !== null && visibleDocumentRevision !== null ? (
-
{document.key}

{document.title}

- {visibleDocumentRevision.changeSummary ?

{visibleDocumentRevision.changeSummary}

: null} -
{visibleDocumentRevision.body}
+
+
+ {document.key} +

{document.title}

+
+ +
+ {visibleDocumentRevision.changeSummary ? ( +

+ {visibleDocumentRevision.changeSummary} +

+ ) : null} +
+                  {visibleDocumentRevision.body}
+                
) : null}
) ) : null} - {tab === "protocol" ?
{protocol.map((entry) =>
{entry.boundary} · {entry.event}
)}
: null} - {tab === "runtime" ?
: null} - {tab === "authority" ?
: null} + {tab === "protocol" ? ( +
+ {protocol.map((entry) => ( +
+ + {entry.boundary} · {entry.event} + + +
+ ))} +
+ ) : null} + {tab === "trace" ? ( + snapshot.providerTrace ? ( +
+

+ {snapshot.providerTrace.status} · expires{" "} + {snapshot.providerTrace.expiresAt} +

+

Raw frame → Interpretation → PRP events → Presentation

+ +
+ ) : ( +
+ +

+ Raw provider capture was off for this run. Canonical protocol + records remain available in Protocol. +

+
+ ) + ) : null} + {tab === "runtime" ? ( +
+ +
+ ) : null} + {tab === "authority" ? ( +
+ +
+ ) : null} ); } diff --git a/packages/paperclip-runner/docs/codex-driver.md b/packages/paperclip-runner/docs/codex-driver.md index 6ee35d0d9c0..9e767b8a0ab 100644 --- a/packages/paperclip-runner/docs/codex-driver.md +++ b/packages/paperclip-runner/docs/codex-driver.md @@ -61,10 +61,14 @@ The model receives one text input containing `paperclip.skillless_task.v1`: - task constraints; and - the expected canonical result schema name. -The thread config explicitly disables automatic skill, app, and collaboration -instruction blocks. The model input accepts only text, never a Codex `skill` -input. The driver captures the returned instruction-source list and requires it -to be empty for the skillless assertion. +The thread config explicitly disables automatic skill and app instruction +blocks. Codex's built-in collaboration instructions are enabled by default so +interactive runs receive native commentary and tool preambles; a driver caller +may explicitly disable them for a specialized deterministic fixture. This +does not enable skills, apps, plugins, memories, or extra model-input kinds. +The model input accepts only text, never a Codex `skill` input. The driver +captures the returned instruction-source list and requires it to be empty for +the skillless assertion. The trusted app-server process has an allowlisted environment. It retains host `HOME` and `CODEX_HOME` only so the provider can authenticate. Model-issued diff --git a/packages/paperclip-runner/docs/tutorials/end-to-end.md b/packages/paperclip-runner/docs/tutorials/end-to-end.md index 0af2d41ad8f..850f0711503 100644 --- a/packages/paperclip-runner/docs/tutorials/end-to-end.md +++ b/packages/paperclip-runner/docs/tutorials/end-to-end.md @@ -101,7 +101,9 @@ pnpm --filter @paperclipai/paperclip-runner verify:rootless tokens. - the Codex child receives an allowlisted environment without Paperclip or OpenAI bearer credentials; -- automatic skill, app, and collaboration instruction blocks are disabled; +- automatic skill and app instruction blocks are disabled, while Codex's + built-in collaboration instructions are enabled by default and remain + explicitly removable for controlled eval baselines; - direct app-server create, resume, read, turn, steer, interrupt, usage, and reconciliation operations preserve stable identities; - provider events normalize to canonical lifecycle, model, tool, file, diff --git a/packages/paperclip-runner/infra/aws-agentcore-paperclip.yaml b/packages/paperclip-runner/infra/aws-agentcore-paperclip.yaml index a7169c8f445..ff5e6a9628c 100644 --- a/packages/paperclip-runner/infra/aws-agentcore-paperclip.yaml +++ b/packages/paperclip-runner/infra/aws-agentcore-paperclip.yaml @@ -25,10 +25,18 @@ Parameters: Type: String Description: Selected foundation-model ARN; the region segment may be * for cross-region inference. AllowedPattern: "^arn:aws(-[^:]+)?:bedrock:[^:]*::foundation-model/.+$" + BedrockMarketplaceProductId: + Type: String + Description: Exact AWS Marketplace product ID for the selected third-party model. + AllowedPattern: "^prod-[a-z0-9]+$" HarnessName: Type: String Default: PaperclipAgentCoreHarness AllowedPattern: "^[a-zA-Z][a-zA-Z0-9_]{0,39}$" + HarnessEndpointName: + Type: String + Default: paperclip + AllowedPattern: "^[a-zA-Z][a-zA-Z0-9_]{0,47}$" MemoryName: Type: String Default: PaperclipAgentCoreMemory @@ -76,6 +84,17 @@ Resources: Resource: - !Ref BedrockModelResourceArn - !Ref BedrockFoundationModelResourceArn + - Sid: ViewMarketplaceSubscriptionsForModelAccess + Effect: Allow + Action: aws-marketplace:ViewSubscriptions + Resource: "*" + - Sid: SubscribePinnedMarketplaceModel + Effect: Allow + Action: aws-marketplace:Subscribe + Resource: "*" + Condition: + StringEquals: + aws-marketplace:ProductId: !Ref BedrockMarketplaceProductId - Sid: ShortTermMemory Effect: Allow Action: @@ -136,6 +155,8 @@ Resources: MaxIterations: 8 MaxTokens: 4096 TimeoutSeconds: 300 + AllowedTools: + - "@*/pc_*" Tools: [] Skills: [] EnvironmentVariables: {} @@ -183,6 +204,8 @@ Resources: - bedrock-agentcore:StopRuntimeSession Resource: - !GetAtt AgentHarness.Arn + - !Sub ${AgentHarness.Arn}/harness-endpoint/${HarnessEndpointName} + - !Sub ${AgentHarness.Arn}/runtime-endpoint/${HarnessEndpointName} - !GetAtt AgentHarness.Environment.AgentCoreRuntimeEnvironment.AgentRuntimeArn - Sid: ReadPinnedHarness Effect: Allow @@ -190,7 +213,9 @@ Resources: - bedrock-agentcore:GetHarness - bedrock-agentcore:GetHarnessEndpoint - bedrock-agentcore:ListHarnessEndpoints - Resource: !GetAtt AgentHarness.Arn + Resource: + - !GetAtt AgentHarness.Arn + - !Sub ${AgentHarness.Arn}/harness-endpoint/${HarnessEndpointName} - Sid: ReadAndPurgeSessionMemory Effect: Allow Action: diff --git a/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json b/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json index 98567b21353..9b4f8c1ffa8 100644 --- a/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json +++ b/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json @@ -13,6 +13,7 @@ "service": { "enum": ["anthropic_managed_agents", "aws_bedrock_agentcore_harness"] }, "providerSessionId": { "type": ["string", "null"], "maxLength": 240 }, "processId": { "type": ["integer", "null"], "minimum": 1 }, + "agentProcessId": { "type": ["integer", "null"], "minimum": 1 }, "endpointArn": { "type": "string", "minLength": 1, "maxLength": 2048 }, "endpointQualifier": { "type": "string", "minLength": 1, "maxLength": 240 }, "memoryId": { "type": "string", "minLength": 1, "maxLength": 240 }, @@ -52,7 +53,7 @@ { "if": { "properties": { "provider": { "const": "acpx" } }, "required": ["provider"] }, "then": { - "required": ["agent", "requestedModel", "acpProtocolVersion", "agentServerPackage", "agentServerVersion", "acpxRecordId"] + "required": ["agent", "requestedModel", "acpProtocolVersion", "agentServerPackage", "agentServerVersion", "acpxRecordId", "agentProcessId"] } } ], diff --git a/packages/paperclip-runner/runner/Cargo.lock b/packages/paperclip-runner/runner/Cargo.lock index 5410ba4c61d..2cdaf04d397 100644 --- a/packages/paperclip-runner/runner/Cargo.lock +++ b/packages/paperclip-runner/runner/Cargo.lock @@ -1785,6 +1785,7 @@ dependencies = [ "aws-sdk-sts", "aws-smithy-types", "aws-types", + "base64", "hmac 0.12.1", "jsonschema", "reqwest", diff --git a/packages/paperclip-runner/runner/Cargo.toml b/packages/paperclip-runner/runner/Cargo.toml index b48cddefb15..68955d49dc5 100644 --- a/packages/paperclip-runner/runner/Cargo.toml +++ b/packages/paperclip-runner/runner/Cargo.toml @@ -14,6 +14,7 @@ hmac = "0.12" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10" +base64 = "0.22" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } jsonschema = { version = "0.50", default-features = false } aws-config = { version = "1.11", features = ["behavior-version-latest"] } diff --git a/packages/paperclip-runner/runner/crates/runner-core/Cargo.toml b/packages/paperclip-runner/runner/crates/runner-core/Cargo.toml index 45cf89d2a0f..937bec89246 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/Cargo.toml +++ b/packages/paperclip-runner/runner/crates/runner-core/Cargo.toml @@ -11,6 +11,7 @@ hmac.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true +base64.workspace = true reqwest.workspace = true jsonschema.workspace = true aws-config.workspace = true diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider.rs index 5c660360d4a..b9f8c2c518c 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider.rs @@ -41,6 +41,7 @@ pub struct AcpxProvider { last_sequence: u64, assistant_text: String, provider_requests: u64, + agent_process_id: Option, } impl AcpxProvider { @@ -71,6 +72,7 @@ impl AcpxProvider { last_sequence: 0, assistant_text: String::new(), provider_requests: 0, + agent_process_id: None, }; provider.request( GeneratedAcpxSidecarCommand::Initialize, @@ -98,6 +100,22 @@ impl AcpxProvider { "ACPX sidecar did not verify the configured effective model", )); } + if let Some(agent_process) = opened.get("agentProcess").filter(|value| value.is_object()) { + provider.agent_process_id = agent_process + .get("pid") + .and_then(Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .filter(|value| *value > 0); + provider.inbox.push_back(ProviderEvent::Notification { + method: "acpx/process".to_owned(), + params: json!({ + "role": "acp_agent", + "pid": agent_process.get("pid").and_then(Value::as_u64), + "processGroupId": agent_process.get("processGroupId").and_then(Value::as_u64), + "startedAt": agent_process.get("startedAt").and_then(Value::as_str), + }), + }); + } provider.request( GeneratedAcpxSidecarCommand::RunAttach, json!({ @@ -322,6 +340,10 @@ impl Provider for AcpxProvider { } } + fn agent_process_id(&self) -> Option { + self.agent_process_id + } + fn session_identity(&self) -> &str { &self.acpx_record_id } @@ -517,7 +539,7 @@ fn validate_config(config: &AcpxProviderConfig) -> Result<(), LocalRunnerError> Some("@earendil-works/pi-coding-agent"), Some("0.84.2"), "openrouter/deepseek/deepseek-v4-flash-0731", - "sha256:5d06b5cfe819b1acd75e8d9dea4766e76a40a64b82549eda08c3835f18bdac24", + "sha256:e806321f458baaf23aa5580324d8f90a59082066105eda69de35b1ef0c8418eb", ), "claude" => ( "@agentclientprotocol/claude-agent-acp", @@ -616,6 +638,13 @@ fn map_runtime_event(payload: Value, provider_requests: u64) -> ProviderEvent { method: "item/started".to_owned(), params: json!({ "item": { "type": "reasoning", "status": "inProgress" } }), }, + "semantic_result" => ProviderEvent::SemanticResult { + result: payload.get("result").cloned().unwrap_or(Value::Null), + item_id: payload + .get("callId") + .and_then(Value::as_str) + .map(str::to_owned), + }, "status" if payload.get("tag").and_then(Value::as_str) == Some("usage_update") => { let breakdown = payload .get("breakdown") @@ -722,4 +751,23 @@ mod tests { _ => panic!("expected notification"), } } + + #[test] + fn maps_runner_owned_terminal_result_without_control_plane_tool_authority() { + let result = json!({ + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "done" + }); + let event = map_runtime_event( + json!({ "type": "semantic_result", "callId": "finish-1", "result": result }), + 1, + ); + match event { + ProviderEvent::SemanticResult { result, item_id } => { + assert_eq!(result["reportedWorkDisposition"], "done"); + assert_eq!(item_id.as_deref(), Some("finish-1")); + } + _ => panic!("expected semantic result"), + } + } } diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/aws_agentcore_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/src/aws_agentcore_provider.rs index d1477a6d9e6..ce076faf261 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/aws_agentcore_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/aws_agentcore_provider.rs @@ -10,14 +10,15 @@ use std::thread::{self, JoinHandle}; use std::time::Duration; use aws_config::sts::AssumeRoleProvider; +use aws_sdk_bedrockagentcore::error::SdkError; use aws_sdk_bedrockagentcore::types::{ - HarnessBedrockApiFormat, HarnessBedrockModelConfig, HarnessContentBlock, - HarnessContentBlockDelta, HarnessContentBlockStart, HarnessConversationRole, - HarnessInlineFunctionConfig, HarnessMessage, HarnessModelConfiguration, HarnessTool, + HarnessContentBlock, HarnessContentBlockDelta, HarnessContentBlockStart, + HarnessConversationRole, HarnessInlineFunctionConfig, HarnessMessage, HarnessTool, HarnessToolConfiguration, HarnessToolResultBlock, HarnessToolResultContentBlock, HarnessToolType, HarnessToolUseBlock, HarnessToolUseStatus, HarnessToolUseType, InvokeHarnessStreamOutput, }; +use aws_smithy_types::error::metadata::ProvideErrorMetadata; use aws_smithy_types::{Document, Number}; use aws_types::region::Region; use jsonschema::validator_for; @@ -32,6 +33,13 @@ use crate::local_runner::LocalRunnerError; use crate::provider_bridge::{AuthorizedTool, ToolResult}; const MAX_AGENTCORE_TOOLS: usize = 64; +// Harness invocations can spend substantial time starting a new runtime before +// response headers (and therefore the event stream) are available. Allow a +// bounded cold start, but leave enough of the eval's outer deadline for the +// durable runner to classify the failure and service runner.shutdown cleanly. +const AGENTCORE_INVOCATION_DELIVERY_TIMEOUT: Duration = Duration::from_secs(120); +#[cfg(test)] +const AGENTCORE_INLINE_TOOL_ALLOWLIST: &str = "@*/pc_*"; #[derive(Clone, Debug)] struct RemoteToolUse { remote_name: String, @@ -132,7 +140,7 @@ impl NetworkWorker { }) .map_err(|_| LocalRunnerError::invalid("AgentCore network worker stopped"))?; reply_rx - .recv_timeout(Duration::from_secs(45)) + .recv_timeout(AGENTCORE_INVOCATION_DELIVERY_TIMEOUT) .map_err(|_| { LocalRunnerError::invalid( "AgentCore invocation delivery is ambiguous and requires Memory reconciliation", @@ -242,17 +250,11 @@ fn network_loop( let actor_id = actor_id.clone(); let events = events.clone(); runtime.spawn(async move { - let model = HarnessBedrockModelConfig::builder() - .model_id(config.model.clone()) - .max_tokens(config.max_output_tokens as i32) - .api_format(HarnessBedrockApiFormat::ConverseStream) - .build(); - let Ok(model) = model else { - let _ = events.send(NetworkEvent::Failure( - "invalid AgentCore model configuration".to_owned(), - )); - return; - }; + // The qualified Harness version is the immutable model + // authority. Supplying a redundant invocation override + // changes AgentCore's authorization path and can require + // caller-side Marketplace permissions, bypassing the + // execution role qualified during provisioning. let response = client .invoke_harness() .harness_arn(config.harness_arn.clone()) @@ -261,8 +263,11 @@ fn network_loop( .runtime_user_id(actor_id.clone()) .actor_id(actor_id.clone()) .set_messages(Some(messages)) - .model(HarnessModelConfiguration::BedrockModelConfig(model)) .set_tools(Some(tools)) + // Invocation-scoped inline functions live behind an + // AgentCore server namespace. Unqualified names are + // silently withheld, so admit each authorized tool as + // `@*/` and nothing else. .set_allowed_tools(Some(allowed_tools)) .set_skills(Some(Vec::new())) .max_iterations(config.max_iterations as i32) @@ -278,7 +283,7 @@ fn network_loop( let mut response = match response { Ok(value) => value, Err(error) => { - let detail = redact_aws_error(&error.to_string()); + let detail = classify_aws_sdk_error(&error); let _ = reply.send(Err(detail.clone())); let _ = events.send(NetworkEvent::Failure(detail)); return; @@ -293,9 +298,8 @@ fn network_loop( } Ok(None) => break, Err(error) => { - let _ = events.send(NetworkEvent::Failure(redact_aws_error( - &error.to_string(), - ))); + let _ = events + .send(NetworkEvent::Failure(classify_aws_sdk_error(&error))); break; } } @@ -312,7 +316,10 @@ fn network_loop( match client .stop_runtime_session() .runtime_session_id(session_id.clone()) - .agent_runtime_arn(config.agent_runtime_arn.clone()) + // Harness-owned Runtime sessions must be addressed by + // the Harness ARN and named Harness endpoint. AWS + // rejects direct use of the generated Runtime ARN. + .agent_runtime_arn(config.harness_arn.clone()) .qualifier(config.endpoint_qualifier.clone()) .client_token(token) .send() @@ -375,7 +382,12 @@ fn normalize_stream_event( if let Some((call_id, remote_name, input)) = tool_blocks.remove(&value.content_block_index()) { - match serde_json::from_str::(&input) { + let parsed = if input.trim().is_empty() { + Ok(json!({})) + } else { + serde_json::from_str::(&input) + }; + match parsed { Ok(input) => { let _ = events.send(NetworkEvent::ToolUse { call_id, @@ -913,9 +925,12 @@ impl Provider for AwsAgentCoreHarnessProvider { })?; let tool_result = HarnessToolResultBlock::builder() .tool_use_id(call_id.clone()) - .content(HarnessToolResultContentBlock::Json(json_to_document( - &delivered.result, - )?)) + // Although the AgentCore data-plane model advertises JSON + // result blocks, the managed Harness runtime currently + // rejects them with `content_type= | unsupported type`. + // Preserve the complete structured result as compact JSON in + // the supported text variant. + .content(encode_tool_result_content(&delivered.result)?) .status(if delivered.is_error { HarnessToolUseStatus::Error } else { @@ -1057,7 +1072,7 @@ fn encode_tools( .config(HarnessToolConfiguration::InlineFunction(inline)) .build() .map_err(|_| LocalRunnerError::invalid("failed to build AgentCore tool"))?; - allowed.push(remote_name); + allowed.push(format!("@*/{remote_name}")); encoded.push(harness_tool); } Ok((encoded, allowed, reverse, schemas)) @@ -1144,6 +1159,58 @@ fn redact_aws_error(value: &str) -> String { } } +fn classify_aws_error_code(code: &str) -> String { + match code { + "AccessDeniedException" | "UnauthorizedException" => { + "AWS AgentCore access denied".to_owned() + } + "ResourceNotFoundException" => "AWS AgentCore resource not found".to_owned(), + "ThrottlingException" | "TooManyRequestsException" => { + "AWS AgentCore request throttled".to_owned() + } + "ConflictException" => "AWS AgentCore resource conflict".to_owned(), + "RequestTimeoutException" | "TimeoutException" => { + "AWS AgentCore request timed out".to_owned() + } + "ValidationException" => "AWS AgentCore request validation failed".to_owned(), + "ServiceUnavailableException" | "InternalServerException" => { + "AWS AgentCore service unavailable".to_owned() + } + _ => "AWS AgentCore request failed".to_owned(), + } +} + +fn classify_aws_sdk_error(error: &SdkError) -> String +where + E: ProvideErrorMetadata, +{ + if let Some(code) = error + .as_service_error() + .and_then(ProvideErrorMetadata::code) + { + return classify_aws_error_code(code); + } + match error { + SdkError::ConstructionFailure(_) => "AWS AgentCore request construction failed".to_owned(), + SdkError::TimeoutError(_) => "AWS AgentCore request timed out".to_owned(), + SdkError::DispatchFailure(context) if context.is_timeout() => { + "AWS AgentCore request dispatch timed out".to_owned() + } + SdkError::DispatchFailure(context) if context.is_io() => { + "AWS AgentCore request network I/O failed".to_owned() + } + SdkError::DispatchFailure(context) if context.is_user() => { + "AWS AgentCore request was rejected before dispatch".to_owned() + } + SdkError::DispatchFailure(_) => { + "AWS AgentCore credential or transport setup failed".to_owned() + } + SdkError::ResponseError(_) => "AWS AgentCore response decoding failed".to_owned(), + SdkError::ServiceError(_) => redact_aws_error(&error.to_string()), + _ => "AWS AgentCore request failed".to_owned(), + } +} + fn is_resource_not_found(value: &str) -> bool { let lower = value.to_ascii_lowercase(); lower.contains("resourcenotfound") @@ -1184,6 +1251,14 @@ fn json_to_document(value: &Value) -> Result { }) } +fn encode_tool_result_content( + value: &Value, +) -> Result { + serde_json::to_string(value) + .map(HarnessToolResultContentBlock::Text) + .map_err(|_| LocalRunnerError::invalid("failed to serialize AgentCore tool result")) +} + #[cfg(test)] mod tests { use super::*; @@ -1211,6 +1286,22 @@ mod tests { assert_eq!(first, remote_tool_name("issues.comment:create")); } + #[test] + fn tool_allowlist_is_confined_to_the_paperclip_inline_namespace() { + let tool = AuthorizedTool { + operation_id: "get_task_context".to_owned(), + version: 1, + description: "Read the assigned task context".to_owned(), + input_schema: json!({"type":"object"}), + response_schema: json!({"type":"object"}), + }; + let (_, allowed, remote, _) = encode_tools(&[tool]).unwrap(); + assert_eq!(AGENTCORE_INLINE_TOOL_ALLOWLIST, "@*/pc_*"); + assert!(remote.keys().all(|name| name.starts_with("pc_"))); + assert_eq!(allowed.len(), 1); + assert!(allowed[0].starts_with("@*/pc_")); + } + #[test] fn rejects_more_than_sixty_four_tools() { let tool = AuthorizedTool { @@ -1234,6 +1325,14 @@ mod tests { redact_aws_error("AccessDeniedException: signed request"), "AWS AgentCore access denied" ); + assert_eq!( + classify_aws_error_code("ValidationException"), + "AWS AgentCore request validation failed" + ); + assert_eq!( + classify_aws_error_code("UnrecognizedFutureError"), + "AWS AgentCore request failed" + ); } #[test] @@ -1345,4 +1444,75 @@ mod tests { other => panic!("unexpected normalized event: {other:?}"), } } + + #[test] + fn eventstream_empty_tool_input_is_normalized_to_an_empty_object() { + let (sender, receiver) = mpsc::sync_channel(4); + let mut blocks = BTreeMap::new(); + let start = HarnessToolUseBlockStart::builder() + .tool_use_id("tool-use-empty") + .name("pc_get_task_context_abc123") + .r#type(HarnessToolUseType::ToolUse) + .build() + .unwrap(); + normalize_stream_event( + InvokeHarnessStreamOutput::ContentBlockStart( + HarnessContentBlockStartEvent::builder() + .content_block_index(1) + .start(HarnessContentBlockStart::ToolUse(start)) + .build() + .unwrap(), + ), + &mut blocks, + &sender, + ); + normalize_stream_event( + InvokeHarnessStreamOutput::ContentBlockDelta( + HarnessContentBlockDeltaEvent::builder() + .content_block_index(1) + .delta(HarnessContentBlockDelta::ToolUse( + HarnessToolUseBlockDelta::builder() + .input("") + .build() + .unwrap(), + )) + .build() + .unwrap(), + ), + &mut blocks, + &sender, + ); + normalize_stream_event( + InvokeHarnessStreamOutput::ContentBlockStop( + HarnessContentBlockStopEvent::builder() + .content_block_index(1) + .build() + .unwrap(), + ), + &mut blocks, + &sender, + ); + match receiver.try_recv().unwrap() { + NetworkEvent::ToolUse { + call_id, + remote_name, + input, + } => { + assert_eq!(call_id, "tool-use-empty"); + assert_eq!(remote_name, "pc_get_task_context_abc123"); + assert_eq!(input, json!({})); + } + other => panic!("unexpected normalized event: {other:?}"), + } + } + + #[test] + fn tool_results_use_the_harness_supported_text_content_variant() { + let value = json!({"ok": true, "nested": {"value": 7}}); + let content = encode_tool_result_content(&value).unwrap(); + let text = content + .as_text() + .expect("AgentCore managed Harness requires text tool results"); + assert_eq!(serde_json::from_str::(text).unwrap(), value); + } } diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs b/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs index 01af2876e94..eb480404448 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs @@ -59,10 +59,10 @@ fn run() -> Result<(), Box> { return Err("authorized tool was not registered".into()); } let config = request.pointer("/params/config").and_then(Value::as_object); - planning_thread = config - .and_then(|value| value.get("include_collaboration_mode_instructions")) - .and_then(Value::as_bool) - == Some(true); + planning_thread = request + .pointer("/params/permissions") + .and_then(Value::as_str) + == Some("paperclip-runner-workspace-read-only"); for key in [ "skills.include_instructions", "include_apps_instructions", diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs index 34745025b3b..89d6ca78cc2 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs @@ -1,25 +1,232 @@ use std::collections::{BTreeMap, VecDeque}; +use std::fs::{self, OpenOptions}; +use std::io::Write; use std::path::{Path, PathBuf}; -use std::time::Duration; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use base64::Engine; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; use crate::local_runner::LocalRunnerError; -use crate::process_supervisor::SupervisedProcess; +use crate::process_supervisor::{ProcessOutput, SupervisedProcess}; use crate::provider_bridge::{AuthorizedTool, ToolResult}; pub const CODEX_APP_SERVER_MAX_FRAME_BYTES: usize = 4 * 1024 * 1024; +const DEFAULT_PROVIDER_TRACE_MAX_BYTES: usize = 64 * 1024 * 1024; + +struct ProviderTraceSink { + sender: Option>, + writer: Option>, + writer_error: Arc>>, + provider: String, + next_frame_id: u64, + next_debug_sequence: u64, + captured_bytes: usize, + max_bytes: usize, + truncated: bool, + incomplete_reason: Option, +} + +impl ProviderTraceSink { + fn from_environment(provider: ProviderKind) -> Option { + let trace_path = std::env::var_os("PAPERCLIP_PROVIDER_TRACE_PATH")?; + let max_bytes = std::env::var("PAPERCLIP_PROVIDER_TRACE_MAX_BYTES") + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_PROVIDER_TRACE_MAX_BYTES); + Some(Self::at_path(trace_path.into(), max_bytes, provider)) + } + + fn at_path(trace_path: PathBuf, max_bytes: usize, provider: ProviderKind) -> Self { + // Debug delivery is deliberately bounded and lossy: a slow trace disk + // may mark the trace incomplete, but it can never backpressure PRP. + let (sender, receiver) = mpsc::sync_channel::(1_024); + let writer_error = Arc::new(Mutex::new(None)); + let thread_error = Arc::clone(&writer_error); + let writer = thread::spawn(move || { + let result = (|| -> std::io::Result<()> { + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&trace_path)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut permissions = file.metadata()?.permissions(); + permissions.set_mode(0o600); + fs::set_permissions(&trace_path, permissions)?; + } + for line in receiver { + file.write_all(line.as_bytes())?; + file.write_all(b"\n")?; + } + file.flush() + })(); + if let Err(error) = result { + if let Ok(mut slot) = thread_error.lock() { + *slot = Some(format!("trace_sidecar_write_failed:{error}")); + } + } + }); + Self { + sender: Some(sender), + writer: Some(writer), + writer_error, + provider: match provider { + ProviderKind::Codex => "codex", + ProviderKind::Opencode => "opencode", + ProviderKind::ClaudeManaged => "claude_managed", + ProviderKind::AwsAgentcore => "aws_agentcore", + ProviderKind::Acpx => "acpx", + } + .to_owned(), + next_frame_id: 1, + next_debug_sequence: 1, + captured_bytes: 0, + max_bytes, + truncated: false, + incomplete_reason: None, + } + } + + fn send_record(&mut self, mut value: Value) { + let Some(sender) = self.sender.as_ref().cloned() else { + return; + }; + if let Some(object) = value.as_object_mut() { + object.insert("debugChannel".to_owned(), json!("rust_native")); + object.insert("debugSequence".to_owned(), json!(self.next_debug_sequence)); + self.next_debug_sequence += 1; + } + match sender.try_send(value.to_string()) { + Ok(()) => {} + Err(mpsc::TrySendError::Full(_)) => { + self.incomplete_reason = Some("trace_sidecar_spool_full".to_owned()); + } + Err(mpsc::TrySendError::Disconnected(_)) => { + self.incomplete_reason = Some("trace_sidecar_channel_closed".to_owned()); + self.sender = None; + } + } + } + + fn frame(&mut self, direction: &str, raw: &[u8]) -> Option { + if self.captured_bytes.saturating_add(raw.len()) > self.max_bytes { + self.truncated = true; + return None; + } + let frame_id = self.next_frame_id; + self.next_frame_id += 1; + self.captured_bytes += raw.len(); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + .to_string(); + self.send_record(json!({ + "kind": "frame", + "schema": "paperclip.provider_trace_frame.v1", + "frameId": frame_id, + "timestamp": timestamp, + "direction": direction, + "transport": "stdio_jsonl", + "provider": self.provider, + "byteLength": raw.len(), + "digest": format!("sha256:{:x}", Sha256::digest(raw)), + "rawBase64": base64::engine::general_purpose::STANDARD.encode(raw), + })); + Some(frame_id) + } + + fn interpretation( + &mut self, + frame_id: u64, + stage: &str, + rule_id: &str, + disposition: &str, + emitted_event_ids: Vec, + dropped_fields: Vec, + reason: &str, + ) { + let field_mappings = dropped_fields + .iter() + .map(|path| { + json!({ + "inputPath": path, + "action": "dropped", + "reason": reason, + }) + }) + .collect::>(); + self.send_record(json!({ + "kind": "interpretation", + "schema": "paperclip.provider_trace_interpretation.v1", + "frameId": frame_id, + "stage": stage, + "ruleId": rule_id, + "disposition": disposition, + "emittedEventIds": emitted_event_ids, + "droppedFields": dropped_fields, + "fieldMappings": field_mappings, + "reason": reason, + })); + } + + fn finish(&mut self) { + if self.sender.is_none() && self.writer.is_none() { + return; + } + let writer_reason = self + .writer_error + .lock() + .ok() + .and_then(|value| value.clone()); + let reason = self.incomplete_reason.clone().or(writer_reason); + let status = if reason.is_some() { + "incomplete" + } else if self.truncated { + "truncated" + } else { + "complete" + }; + let acknowledged_debug_sequence = self.next_debug_sequence.saturating_sub(1); + self.send_record(json!({ + "kind": "trace_status", + "status": status, + "acknowledgedDebugSequence": acknowledged_debug_sequence, + "reason": reason.or_else(|| self.truncated.then(|| "provider_trace_max_bytes_exceeded".to_owned())), + })); + self.sender.take(); + if let Some(writer) = self.writer.take() { + let _ = writer.join(); + } + } +} + +impl Drop for ProviderTraceSink { + fn drop(&mut self) { + self.finish(); + } +} fn default_collaboration_mode() -> String { "default".to_owned() } -fn skillless_thread_config(collaboration_mode: &str) -> Value { +fn default_collaboration_mode_instructions() -> bool { + true +} + +fn skillless_thread_config(include_collaboration_mode_instructions: bool) -> Value { json!({ "skills.include_instructions": false, "include_apps_instructions": false, - "include_collaboration_mode_instructions": collaboration_mode == "plan", + "include_collaboration_mode_instructions": include_collaboration_mode_instructions, "features.apps": false, "features.plugins": false, "features.multi_agent": false, @@ -52,6 +259,8 @@ pub struct LocalProviderConfig { pub instructions: String, #[serde(default = "default_collaboration_mode")] pub collaboration_mode: String, + #[serde(default = "default_collaboration_mode_instructions")] + pub include_collaboration_mode_instructions: bool, } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] @@ -250,10 +459,13 @@ pub enum ProviderSessionIdentity { pub trait Provider { fn kind(&self) -> ProviderKind; + fn runtime_identity(&self) -> ProviderRuntimeIdentity; + /// PID of a provider-owned child below the primary runtime process, when + /// the provider has a stable child identity to report. This is metadata, + /// never an ownership or authorization boundary. fn agent_process_id(&self) -> Option { None } - fn runtime_identity(&self) -> ProviderRuntimeIdentity; fn session_identity(&self) -> &str; fn provider_session_id(&self) -> Option<&str>; fn durable_session_identity(&self) -> Option { @@ -262,6 +474,20 @@ pub trait Provider { fn durable_event_cursor(&self) -> Option<&str> { None } + fn take_provider_trace_frame_id(&mut self) -> Option { + None + } + fn record_provider_trace_interpretation( + &mut self, + _frame_id: u64, + _stage: &str, + _rule_id: &str, + _disposition: &str, + _emitted_event_ids: Vec, + _dropped_fields: Vec, + _reason: &str, + ) { + } fn configure_tools(&mut self, _tools: Vec) -> Result<(), LocalRunnerError> { Ok(()) } @@ -341,10 +567,12 @@ pub struct CodexProvider { thread_id: String, session_id: Option, active_provider_turn_id: Option, - pending_messages: VecDeque, + pending_messages: VecDeque<(Value, Option)>, pending_tool_requests: BTreeMap, collaboration_mode: String, collaboration_mode_payload: Option, + trace: Option, + last_trace_frame_id: Option, } impl CodexProvider { @@ -373,12 +601,14 @@ impl CodexProvider { pending_tool_requests: BTreeMap::new(), collaboration_mode: config.collaboration_mode.clone(), collaboration_mode_payload: None, + trace: ProviderTraceSink::from_environment(config.kind), + last_trace_frame_id: None, }; let initialized = provider.request("initialize", json!({ "clientInfo": { "name": "paperclip-runnerd", "title": "Paperclip Runner", "version": "1" }, "capabilities": { "experimentalApi": true, "requestAttestation": false } }))?; - provider.process.send(&json!({"method": "initialized"}))?; + provider.send_frame(&json!({"method": "initialized"}))?; let dynamic_tools = tools .map(|tool| { json!({ @@ -408,7 +638,7 @@ impl CodexProvider { "approvalPolicy": "never", "permissions": permission_profile, "runtimeWorkspaceRoots": [config.cwd], - "config": skillless_thread_config(&config.collaboration_mode), + "config": skillless_thread_config(config.include_collaboration_mode_instructions), "baseInstructions": config.instructions, "dynamicTools": dynamic_tools, "experimentalRawEvents": true, @@ -424,7 +654,7 @@ impl CodexProvider { "approvalPolicy": "never", "permissions": permission_profile, "runtimeWorkspaceRoots": [config.cwd], - "config": skillless_thread_config(&config.collaboration_mode), + "config": skillless_thread_config(config.include_collaboration_mode_instructions), "baseInstructions": config.instructions, "dynamicTools": dynamic_tools, "experimentalRawEvents": true, @@ -567,20 +797,63 @@ impl CodexProvider { } pub fn poll(&mut self) -> Result, LocalRunnerError> { - let message = if let Some(message) = self.pending_messages.pop_front() { + let (message, frame_id) = if let Some(message) = self.pending_messages.pop_front() { message } else { - let Some(line) = self.process.receive_stdout_line(Duration::from_millis(1))? else { + let Some(line) = self.receive_stdout_line(Duration::from_millis(1))? else { return if self.process.try_wait()?.is_some() { Ok(Some(ProviderEvent::Exited)) } else { Ok(None) }; }; - serde_json::from_str(&line).map_err(|error| { + let frame_id = self.trace_inbound(&line); + let message = serde_json::from_str(&line).map_err(|error| { + if let (Some(trace), Some(frame_id)) = (self.trace.as_mut(), frame_id) { + trace.interpretation( + frame_id, + "rust_jsonrpc_parse", + "codex.jsonrpc.invalid", + "rejected", + Vec::new(), + Vec::new(), + "Provider frame was not valid JSON-RPC", + ); + } LocalRunnerError::invalid(format!("Codex emitted invalid JSON-RPC: {error}")) - })? + })?; + (message, frame_id) }; + self.last_trace_frame_id = frame_id; + if let (Some(trace), Some(frame_id)) = (self.trace.as_mut(), frame_id) { + let method = message.get("method").and_then(Value::as_str); + let disposition = if method.is_some() { + "mapped" + } else { + "ignored" + }; + trace.interpretation( + frame_id, + "rust_jsonrpc_parse", + method + .map(|value| { + if value == "item/tool/call" { + "codex.tool_call" + } else { + "codex.notification" + } + }) + .unwrap_or("codex.unroutable"), + disposition, + Vec::new(), + Vec::new(), + if method.is_some() { + "JSON-RPC frame parsed and routed to provider normalization" + } else { + "JSON-RPC frame had no routable method" + }, + ); + } if message.get("id").is_some() && message.get("method").and_then(Value::as_str) == Some("item/tool/call") { @@ -638,7 +911,7 @@ impl CodexProvider { .ok_or_else(|| { LocalRunnerError::invalid("Codex tool result has no pending JSON-RPC request") })?; - self.process.send(&json!({ + self.send_frame(&json!({ "id": request_id, "result": { "success": !result.is_error, @@ -654,25 +927,114 @@ impl CodexProvider { /// `Drop` remains the last-resort cleanup path, but runner shutdown must not /// depend on an implicit destructor firing after the durable transport loop. pub fn shutdown(&mut self) -> Result<(), LocalRunnerError> { - self.process.terminate_group().map(|_| ()) + let result = self.process.terminate_group().map(|_| ()); + if let Some(trace) = self.trace.as_mut() { + trace.finish(); + } + result + } + + fn send_frame(&mut self, value: &Value) -> Result<(), LocalRunnerError> { + if let Some(trace) = self.trace.as_mut() { + let raw = serde_json::to_vec(value).unwrap_or_default(); + if let Some(frame_id) = trace.frame("client_to_provider", &raw) { + trace.interpretation( + frame_id, + "rust_native_transport", + "codex.jsonrpc.outbound", + "operator_only", + Vec::new(), + Vec::new(), + "Outbound provider command does not enter the canonical PRP outbox", + ); + } + } + self.process.send(value) + } + + fn trace_inbound(&mut self, line: &str) -> Option { + self.trace + .as_mut() + .and_then(|trace| trace.frame("provider_to_client", line.as_bytes())) + } + + fn receive_stdout_line( + &mut self, + timeout: Duration, + ) -> Result, LocalRunnerError> { + let deadline = Instant::now() + timeout; + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(None); + } + match self.process.recv_timeout(remaining) { + Ok(ProcessOutput::Stdout(line)) => return Ok(Some(line)), + Ok(ProcessOutput::Stderr(line)) => { + if let Some(trace) = self.trace.as_mut() { + if let Some(frame_id) = trace.frame("provider_stderr", line.as_bytes()) { + trace.interpretation( + frame_id, + "rust_native_transport", + "provider.stderr", + "operator_only", + Vec::new(), + Vec::new(), + "Provider stderr is retained only in the restricted trace sidecar", + ); + } + } + } + Ok(ProcessOutput::StdoutError(message)) => { + return Err(LocalRunnerError::invalid(message)) + } + Ok(ProcessOutput::StdoutClosed) => return Ok(None), + Ok(ProcessOutput::StderrClosed) => {} + Err(mpsc::RecvTimeoutError::Timeout) => return Ok(None), + Err(mpsc::RecvTimeoutError::Disconnected) => { + return Err(LocalRunnerError::invalid("process output channel closed")) + } + } + } } fn request(&mut self, method: &str, params: Value) -> Result { let id = self.next_request_id; self.next_request_id += 1; - self.process - .send(&json!({"id": id, "method": method, "params": params}))?; + self.send_frame(&json!({"id": id, "method": method, "params": params}))?; loop { let line = self - .process .receive_stdout_line(Duration::from_secs(30))? .ok_or_else(|| { LocalRunnerError::invalid(format!("Codex {method} response timed out")) })?; + let frame_id = self.trace_inbound(&line); let message: Value = serde_json::from_str(&line).map_err(|error| { + if let (Some(trace), Some(frame_id)) = (self.trace.as_mut(), frame_id) { + trace.interpretation( + frame_id, + "rust_jsonrpc_parse", + "codex.jsonrpc.invalid", + "rejected", + Vec::new(), + Vec::new(), + "Provider frame was not valid JSON-RPC", + ); + } LocalRunnerError::invalid(format!("Codex emitted invalid JSON-RPC: {error}")) })?; if message.get("id").and_then(Value::as_u64) == Some(id) { + if let (Some(trace), Some(frame_id)) = (self.trace.as_mut(), frame_id) { + trace.interpretation( + frame_id, + "rust_jsonrpc_parse", + "codex.jsonrpc.response", + "operator_only", + Vec::new(), + Vec::new(), + "Matched synchronous app-server response", + ); + } if let Some(error) = message.get("error") { return Err(LocalRunnerError::invalid(format!( "Codex {method} failed: {error}" @@ -680,7 +1042,7 @@ impl CodexProvider { } return Ok(message.get("result").cloned().unwrap_or(Value::Null)); } - self.pending_messages.push_back(message); + self.pending_messages.push_back((message, frame_id)); } } } @@ -701,6 +1063,31 @@ impl Provider for CodexProvider { fn provider_session_id(&self) -> Option<&str> { self.session_id() } + fn take_provider_trace_frame_id(&mut self) -> Option { + self.last_trace_frame_id.take() + } + fn record_provider_trace_interpretation( + &mut self, + frame_id: u64, + stage: &str, + rule_id: &str, + disposition: &str, + emitted_event_ids: Vec, + dropped_fields: Vec, + reason: &str, + ) { + if let Some(trace) = self.trace.as_mut() { + trace.interpretation( + frame_id, + stage, + rule_id, + disposition, + emitted_event_ids, + dropped_fields, + reason, + ); + } + } fn start_turn( &mut self, message: &str, @@ -709,12 +1096,12 @@ impl Provider for CodexProvider { ) -> Result { CodexProvider::start_turn(self, message, cwd) } - fn steer_turn(&mut self, turn_id: &str, message: &str) -> Result { - CodexProvider::steer_turn(self, turn_id, message) - } fn interrupt_turn(&mut self, turn_id: &str) -> Result { CodexProvider::interrupt_turn(self, turn_id) } + fn steer_turn(&mut self, turn_id: &str, message: &str) -> Result { + CodexProvider::steer_turn(self, turn_id, message) + } fn read(&mut self) -> Result { self.read_thread() } @@ -728,3 +1115,130 @@ impl Provider for CodexProvider { CodexProvider::shutdown(self) } } + +#[cfg(test)] +mod provider_trace_tests { + use super::*; + + #[test] + fn collaboration_instructions_default_on_and_allow_explicit_opt_out() { + let default_config: LocalProviderConfig = serde_json::from_value(json!({ + "kind": "codex", + "command": "codex", + "args": [], + "cwd": "/workspace", + "model": null, + "instructions": "Run the task.", + "collaborationMode": "default" + })) + .unwrap(); + assert!(default_config.include_collaboration_mode_instructions); + assert_eq!( + skillless_thread_config(default_config.include_collaboration_mode_instructions) + ["include_collaboration_mode_instructions"], + json!(true) + ); + + let opt_out_config: LocalProviderConfig = serde_json::from_value(json!({ + "kind": "codex", + "command": "codex", + "args": [], + "cwd": "/workspace", + "model": null, + "instructions": "Run the eval fixture.", + "collaborationMode": "default", + "includeCollaborationModeInstructions": false + })) + .unwrap(); + assert!(!opt_out_config.include_collaboration_mode_instructions); + assert_eq!( + skillless_thread_config(opt_out_config.include_collaboration_mode_instructions) + ["include_collaboration_mode_instructions"], + json!(false) + ); + } + + #[test] + fn provider_trace_preserves_exact_bytes_order_digest_and_interpretation() { + let trace_path = std::env::temp_dir().join(format!( + "paperclip-provider-trace-{}.ndjson", + uuid::Uuid::new_v4() + )); + let first = br#"{"id":1,"method":"thread/start"}"#; + let second = br#"{"method":"item/completed","params":{"item":{"type":"agentMessage"}}}"#; + let mut trace = ProviderTraceSink::at_path( + trace_path.clone(), + DEFAULT_PROVIDER_TRACE_MAX_BYTES, + ProviderKind::Codex, + ); + + let first_id = trace.frame("client_to_provider", first).unwrap(); + let second_id = trace.frame("provider_to_client", second).unwrap(); + trace.interpretation( + second_id, + "rust_jsonrpc_parse", + "codex.notification", + "mapped", + vec!["event_runner_000001".to_owned()], + Vec::new(), + "mapped for test", + ); + trace.finish(); + + let entries = fs::read_to_string(&trace_path) + .unwrap() + .lines() + .map(|line| serde_json::from_str::(line).unwrap()) + .collect::>(); + let frames = entries + .iter() + .filter(|entry| entry["kind"] == "frame") + .collect::>(); + assert_eq!((first_id, second_id), (1, 2)); + assert_eq!( + base64::engine::general_purpose::STANDARD + .decode(frames[0]["rawBase64"].as_str().unwrap()) + .unwrap(), + first + ); + assert_eq!( + base64::engine::general_purpose::STANDARD + .decode(frames[1]["rawBase64"].as_str().unwrap()) + .unwrap(), + second + ); + assert_eq!( + frames[1]["digest"], + format!("sha256:{:x}", Sha256::digest(second)) + ); + assert_eq!(entries[0]["debugSequence"], 1); + assert_eq!(entries[1]["debugSequence"], 2); + assert_eq!(entries[2]["debugSequence"], 3); + assert_eq!(entries[2]["emittedEventIds"][0], "event_runner_000001"); + assert_eq!(entries.last().unwrap()["status"], "complete"); + assert_eq!( + entries.last().unwrap()["acknowledgedDebugSequence"], + entries.len() - 1 + ); + + let _ = fs::remove_file(trace_path); + } + + #[test] + fn provider_trace_truncation_does_not_accept_bytes_past_the_cap() { + let trace_path = std::env::temp_dir().join(format!( + "paperclip-provider-trace-{}.ndjson", + uuid::Uuid::new_v4() + )); + let mut trace = ProviderTraceSink::at_path(trace_path.clone(), 4, ProviderKind::Codex); + assert!(trace.frame("provider_to_client", b"12345").is_none()); + trace.finish(); + + let content = fs::read_to_string(&trace_path).unwrap(); + assert!(!content.contains("rawBase64")); + assert!(content.contains("\"status\":\"truncated\"")); + assert!(content.contains("provider_trace_max_bytes_exceeded")); + + let _ = fs::remove_file(trace_path); + } +} diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs b/packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs index ba82d0652c7..76a0b0d16b5 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs @@ -641,6 +641,8 @@ fn provider_descriptor( "provider": provider_kind_name(value.kind), "driver": if value.kind == crate::codex_provider::ProviderKind::Opencode { "opencode_server" } else { "codex_app_server" }, "model": value.model, + "collaborationMode": value.collaboration_mode, + "collaborationInstructions": value.include_collaboration_mode_instructions, "executionKind": "local_process", "providerVersion": if value.kind == crate::codex_provider::ProviderKind::Opencode { "1.18.17" } else { "unqualified" }, "providerSessionId": runtime.and_then(|item| item.provider_session_id()), @@ -1044,6 +1046,49 @@ fn poll_provider( ))); } }; + let trace_frame_id = runtime.take_provider_trace_frame_id(); + let trace_first_source_seq = state.next_source_seq; + let (trace_rule_id, trace_disposition, trace_dropped_fields, trace_reason) = match &event { + crate::codex_provider::ProviderEvent::ToolCall { .. } => ( + "provider.tool_call", + "mapped", + Vec::new(), + "Provider tool call mapped to semantic_tool.input", + ), + crate::codex_provider::ProviderEvent::SemanticResult { .. } => ( + "provider.semantic_result", + "mapped", + Vec::new(), + "Provider semantic result mapped independently from final prose", + ), + crate::codex_provider::ProviderEvent::RuntimeRequest { .. } => ( + "provider.runtime_request", + "mapped", + Vec::new(), + "Provider request mapped to runtime_request.created", + ), + crate::codex_provider::ProviderEvent::Notification { method, .. } if matches!( + method.as_str(), + "turn/started" | "turn/completed" | "item/started" | "item/delta" | "item/completed" | "thread/tokenUsage/updated" | "provider/budgetReached" | "acpx/process" + ) => ( + "provider.notification.known", + "mapped", + Vec::new(), + "Known provider notification mapped to canonical PRP event", + ), + crate::codex_provider::ProviderEvent::Notification { .. } => ( + "provider.notification.unknown", + "generic", + vec!["params".to_owned()], + "Unknown provider notification retained as harness.diagnostic without provider-only fields", + ), + crate::codex_provider::ProviderEvent::Exited => ( + "provider.process.exited", + "rejected", + Vec::new(), + "Provider exited before a new frame could be mapped", + ), + }; match event { crate::codex_provider::ProviderEvent::ToolCall { call_id, @@ -1231,6 +1276,20 @@ fn poll_provider( )); } } + if let Some(frame_id) = trace_frame_id { + let emitted_event_ids = (trace_first_source_seq..state.next_source_seq) + .map(|source_seq| format!("event_{}_{source_seq:06}", state.runner_instance_id)) + .collect::>(); + runtime.record_provider_trace_interpretation( + frame_id, + "rust_durable_normalization", + trace_rule_id, + trace_disposition, + emitted_event_ids, + trace_dropped_fields, + trace_reason, + ); + } if let Some(cursor) = runtime.durable_event_cursor() { state.provider_event_cursor = Some(cursor.to_owned()); store.save(state)?; diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/durable/state.rs b/packages/paperclip-runner/runner/crates/runner-core/src/durable/state.rs index ba96e9d5273..c3fe6e4a186 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/durable/state.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/durable/state.rs @@ -1528,6 +1528,32 @@ fn execute_command_effect( 1, "provider turn interruption requested".to_owned(), )), + "request.resolve" if state.provider_config.is_some() => { + let request_id = payload + .get("requestId") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + DurableRunnerError::invalid("request.resolve payload.requestId is required") + })?; + let action = payload + .pointer("/resolution/action") + .or_else(|| payload.get("action")) + .and_then(Value::as_str) + .unwrap_or("cancel"); + if !["accept", "accept_for_session", "decline", "cancel"].contains(&action) { + return Ok(( + "rejected".to_owned(), + 0, + "provider runtime request resolution action is unsupported".to_owned(), + )); + } + Ok(( + "completed".to_owned(), + 0, + format!("provider runtime request {request_id} resolution admitted"), + )) + } "provider.thread.read" => Ok(( "completed".to_owned(), 1, diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/provider_events.rs b/packages/paperclip-runner/runner/crates/runner-core/src/provider_events.rs index 10221603c38..2a788b0903d 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/provider_events.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/provider_events.rs @@ -135,6 +135,47 @@ pub fn canonical_provider_events(method: &str, params: &Value) -> Vec<(String, V _ => "other", }; let url = text(action.get("url")); + let sources: Vec = if complete { + item.get("results") + .and_then(Value::as_array) + .map(|results| { + results + .iter() + .take(64) + .enumerate() + .filter_map(|(index, result)| { + let source_url = text(result.get("url")); + if (!source_url.starts_with("http://") + && !source_url.starts_with("https://")) + || source_url.len() > 8192 + { + return None; + } + let fallback_id = format!("{item_id}:source:{}", index + 1); + let source_id = id( + text(result.get("ref_id").or_else(|| result.get("refId"))), + &fallback_id, + ); + let title = redact_text(text(result.get("title"))); + let title: String = if title.is_empty() { + source_url.chars().take(4000).collect() + } else { + title.chars().take(4000).collect() + }; + let snippet = redact_text(text(result.get("snippet"))); + Some(json!({ + "sourceId": source_id, + "title": title, + "url": source_url, + "snippet": if snippet.is_empty() { Value::Null } else { Value::String(snippet.chars().take(4000).collect()) }, + })) + }) + .collect() + }) + .unwrap_or_default() + } else { + Vec::new() + }; push( &mut result, if complete { @@ -142,7 +183,7 @@ pub fn canonical_provider_events(method: &str, params: &Value) -> Vec<(String, V } else { "research.started" }, - json!({"schema":"paperclip.research.v1","researchId":item_id,"action":action_kind,"status":if complete {"completed"} else {"running"},"query":item.get("query").or_else(|| action.get("query")).and_then(Value::as_str),"url":if url.starts_with("http://") || url.starts_with("https://") {Value::String(url.to_owned())} else {Value::Null},"pattern":action.get("pattern").and_then(Value::as_str),"sources":[]}), + json!({"schema":"paperclip.research.v1","researchId":item_id,"action":action_kind,"status":if complete {"completed"} else {"running"},"query":item.get("query").or_else(|| action.get("query")).and_then(Value::as_str),"url":if url.starts_with("http://") || url.starts_with("https://") {Value::String(url.to_owned())} else {Value::Null},"pattern":action.get("pattern").and_then(Value::as_str),"sources":sources}), &item_id, ); } else if (method == "item/started" || complete) @@ -332,4 +373,19 @@ mod tests { assert_eq!(events[0].1["available"], false); assert!(!events[0].1.to_string().contains("native-secret")); } + + #[test] + fn maps_bounded_safe_web_search_sources() { + let events = canonical_provider_events( + "item/completed", + &json!({"item":{"id":"web-1","type":"webSearch","results":[ + {"ref_id":"source-1","title":"Protocol notes","url":"https://example.com/prp","snippet":"Canonical event details"}, + {"ref_id":"unsafe","title":"Unsafe","url":"file:///etc/passwd"} + ]}}), + ); + assert_eq!(events[0].0, "research.completed"); + assert_eq!(events[0].1["sources"].as_array().unwrap().len(), 1); + assert_eq!(events[0].1["sources"][0]["sourceId"], "source-1"); + assert_eq!(events[0].1["sources"][0]["url"], "https://example.com/prp"); + } } diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs index 2db332c2f15..d6ec09b1f75 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs @@ -28,6 +28,7 @@ fn codex_plan_mode_is_qualified_and_selected_on_turn_start() { model: None, instructions: "Author a plan without editing files.".to_owned(), collaboration_mode: "plan".to_owned(), + include_collaboration_mode_instructions: true, }, vec![test_tool()].into_iter(), None, @@ -48,6 +49,7 @@ fn codex_dynamic_tool_round_trips_through_the_rust_provider_boundary() { model: None, instructions: "Use the authorized Paperclip tools.".to_owned(), collaboration_mode: "default".to_owned(), + include_collaboration_mode_instructions: true, }, vec![AuthorizedTool { operation_id: "get_task_context".to_owned(), @@ -124,6 +126,7 @@ fn provider_contract_preserves_the_opencode_tag() { model: Some("openrouter/deepseek/deepseek-v4-flash-0731".to_owned()), instructions: "Use the authorized Paperclip tools.".to_owned(), collaboration_mode: "default".to_owned(), + include_collaboration_mode_instructions: false, }, vec![AuthorizedTool { operation_id: "get_task_context".to_owned(), @@ -151,6 +154,7 @@ fn image_like_prompt_stays_skillless_and_accepts_a_valid_one_point_two_megabyte_ model: None, instructions: "Use only the authorized Paperclip tools.".to_owned(), collaboration_mode: "default".to_owned(), + include_collaboration_mode_instructions: true, }, vec![AuthorizedTool { operation_id: "get_task_context".to_owned(), @@ -210,6 +214,7 @@ fn codex_provider_rejects_an_event_above_the_four_megabyte_hard_limit() { model: None, instructions: "Use only the authorized Paperclip tools.".to_owned(), collaboration_mode: "default".to_owned(), + include_collaboration_mode_instructions: true, }, vec![AuthorizedTool { operation_id: "get_task_context".to_owned(), diff --git a/packages/paperclip-runner/scripts/aws-agentcore-provisioning.test.mjs b/packages/paperclip-runner/scripts/aws-agentcore-provisioning.test.mjs index f73139c500e..397a51f25a5 100644 --- a/packages/paperclip-runner/scripts/aws-agentcore-provisioning.test.mjs +++ b/packages/paperclip-runner/scripts/aws-agentcore-provisioning.test.mjs @@ -25,10 +25,17 @@ test("AgentCore template has closed development/private resources and explicit c assert.match(source, /MaxIterations: 8/); assert.match(source, /MaxTokens: 4096/); assert.match(source, /TimeoutSeconds: 300/); + assert.match(source, /AllowedTools:\s+- "@\*\/pc_\*"/); assert.match(source, /Tools: \[\]/); assert.match(source, /Skills: \[\]/); assert.doesNotMatch(source, /BedrockModelResourceArn:\s+[\s\S]{0,100}Default: "\*"/); assert.match(source, /BedrockFoundationModelResourceArn/); + assert.match(source, /HarnessEndpointName/); + assert.match(source, /\$\{AgentHarness\.Arn\}\/harness-endpoint\/\$\{HarnessEndpointName\}/); + assert.match(source, /\$\{AgentHarness\.Arn\}\/runtime-endpoint\/\$\{HarnessEndpointName\}/); + assert.match(source, /BedrockMarketplaceProductId/); + assert.match(source, /Sid: ViewMarketplaceSubscriptionsForModelAccess[\s\S]*Action: aws-marketplace:ViewSubscriptions[\s\S]*Sid: SubscribePinnedMarketplaceModel[\s\S]*Action: aws-marketplace:Subscribe[\s\S]*aws-marketplace:ProductId: !Ref BedrockMarketplaceProductId/); + assert.doesNotMatch(source, /aws-marketplace:Unsubscribe/); }); test("AgentCore wrapper is valid shell and writes only nonsecret profile metadata", async () => { @@ -38,6 +45,7 @@ test("AgentCore wrapper is valid shell and writes only nonsecret profile metadat assert.ok(generatedBlock.length > 0); assert.doesNotMatch(generatedBlock, /AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN|Authorization|X-Amz-Signature/); assert.match(source, /chmod 600 "\$tmp"/); + assert.match(source, /printf 'AWS_CONFIG_FILE=%s\\n'/); assert.match(source, /cloudformation deploy/); assert.match(source, /CAPABILITY_NAMED_IAM/); assert.match(source, /--\) shift ;;/); @@ -45,5 +53,12 @@ test("AgentCore wrapper is valid shell and writes only nonsecret profile metadat assert.doesNotMatch(source, /printf 'arn:%s:iam::%s:role\/%s/); assert.match(source, /existing_status.*ROLLBACK_COMPLETE/s); assert.match(source, /cloudformation wait stack-delete-complete/); - assert.ok(source.indexOf("delete-harness-endpoint") < source.indexOf("cloudformation delete-stack")); + assert.match(source, /--query harness\.status/); + assert.match(source, /AgentCore tool allowlist drift/); + assert.match(source, /--query memory\.status/); + assert.match(source, /--query endpoint\.status/); + assert.match(source, /o\.endpoint\?\.arn/); + assert.match(source, /--marketplace-product-id/); + assert.match(source, /BedrockMarketplaceProductId=\$MARKETPLACE_PRODUCT_ID/); + assert.ok(source.indexOf("delete-harness-endpoint") < source.lastIndexOf("cloudformation delete-stack")); }); diff --git a/packages/paperclip-runner/scripts/aws-agentcore.sh b/packages/paperclip-runner/scripts/aws-agentcore.sh index eb4b9b3e8fc..c62a74a6d82 100755 --- a/packages/paperclip-runner/scripts/aws-agentcore.sh +++ b/packages/paperclip-runner/scripts/aws-agentcore.sh @@ -15,6 +15,8 @@ DEPLOYMENT_MODE="development" STACK_NAME="paperclip-agentcore-development" ENDPOINT_NAME="paperclip" MODEL_ID="global.anthropic.claude-sonnet-4-6" +MARKETPLACE_PRODUCT_ID="prod-ffvjxvh4ltq64" +MARKETPLACE_PRODUCT_ID_EXPLICIT=false TRUSTED_PRINCIPAL="" DRY_RUN=false FORCE=false @@ -28,6 +30,7 @@ usage() { " --mode MODE development or private" \ " --stack-name NAME CloudFormation stack name" \ " --model MODEL_ID Bedrock-native model ID" \ + " --marketplace-product-id ID exact AWS Marketplace product ID for the model" \ " --principal ARN stable IAM role/user trusted to assume runner role" \ " --dry-run validate and print changes without deployment" \ " --yes confirm destructive teardown" \ @@ -42,6 +45,7 @@ while [[ $# -gt 0 ]]; do --mode) DEPLOYMENT_MODE="$2"; shift 2 ;; --stack-name) STACK_NAME="$2"; shift 2 ;; --model) MODEL_ID="$2"; shift 2 ;; + --marketplace-product-id) MARKETPLACE_PRODUCT_ID="$2"; MARKETPLACE_PRODUCT_ID_EXPLICIT=true; shift 2 ;; --principal) TRUSTED_PRINCIPAL="$2"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; --force) FORCE=true; shift ;; @@ -67,6 +71,14 @@ if [[ ! "$AWS_REGION_NAME" =~ ^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$ || "$AWS_REGION_NA printf 'This proof of concept currently supports commercial AWS regions only.\n' >&2 exit 2 fi +if [[ "$MODEL_ID" != "global.anthropic.claude-sonnet-4-6" && "$MARKETPLACE_PRODUCT_ID_EXPLICIT" != true ]]; then + printf 'A custom --model requires its exact --marketplace-product-id so subscription authority remains model-scoped.\n' >&2 + exit 2 +fi +if [[ ! "$MARKETPLACE_PRODUCT_ID" =~ ^prod-[a-z0-9]+$ ]]; then + printf 'Marketplace product ID must use the AWS prod-* form.\n' >&2 + exit 2 +fi aws_cli() { aws --profile "$AWS_PROFILE_NAME" --region "$AWS_REGION_NAME" --no-cli-pager "$@" @@ -93,7 +105,7 @@ stack_output() { wait_for_harness_status() { local harness_id="$1" harness_version="$2" status="" for _ in {1..60}; do - status="$(aws_cli bedrock-agentcore-control get-harness --harness-id "$harness_id" --harness-version "$harness_version" --query status --output text 2>/dev/null || true)" + status="$(aws_cli bedrock-agentcore-control get-harness --harness-id "$harness_id" --harness-version "$harness_version" --query harness.status --output text 2>/dev/null || true)" if [[ "$status" == "READY" ]]; then return 0; fi if [[ "$status" == "FAILED" || "$status" == "CREATE_FAILED" || "$status" == "UPDATE_FAILED" ]]; then printf 'Harness %s entered terminal status %s.\n' "$harness_id" "$status" >&2 @@ -108,7 +120,7 @@ wait_for_harness_status() { wait_for_memory_status() { local memory_id="$1" status="" for _ in {1..60}; do - status="$(aws_cli bedrock-agentcore-control get-memory --memory-id "$memory_id" --query status --output text 2>/dev/null || true)" + status="$(aws_cli bedrock-agentcore-control get-memory --memory-id "$memory_id" --query memory.status --output text 2>/dev/null || true)" [[ "$status" == "ACTIVE" ]] && return 0 [[ "$status" == "FAILED" ]] && { printf 'Memory %s entered FAILED.\n' "$memory_id" >&2; return 1; } sleep 5 @@ -120,7 +132,7 @@ wait_for_memory_status() { wait_for_endpoint_status() { local harness_id="$1" endpoint_name="$2" status="" for _ in {1..60}; do - status="$(aws_cli bedrock-agentcore-control get-harness-endpoint --harness-id "$harness_id" --endpoint-name "$endpoint_name" --query status --output text 2>/dev/null || true)" + status="$(aws_cli bedrock-agentcore-control get-harness-endpoint --harness-id "$harness_id" --endpoint-name "$endpoint_name" --query endpoint.status --output text 2>/dev/null || true)" [[ "$status" == "READY" ]] && return 0 [[ "$status" == "FAILED" ]] && { printf 'Harness endpoint %s entered FAILED.\n' "$endpoint_name" >&2; return 1; } sleep 5 @@ -184,12 +196,21 @@ assume_runner_role() { probe() { load_local_env - local profile_save="$AWS_PROFILE_NAME" + local profile_save="$AWS_PROFILE_NAME" harness_json assume_runner_role "$PAPERCLIP_AWS_AGENTCORE_INVOCATION_ROLE_ARN" AWS_PROFILE_NAME="" - aws --region "$AWS_REGION_NAME" --no-cli-pager bedrock-agentcore-control get-harness \ + harness_json="$(aws --region "$AWS_REGION_NAME" --no-cli-pager bedrock-agentcore-control get-harness \ --harness-id "$PAPERCLIP_AWS_AGENTCORE_HARNESS_ID" \ - --harness-version "$PAPERCLIP_AWS_AGENTCORE_HARNESS_VERSION" >/dev/null + --harness-version "$PAPERCLIP_AWS_AGENTCORE_HARNESS_VERSION" --output json)" + printf '%s' "$harness_json" | node -e ' + const fs = require("fs"); + const h = JSON.parse(fs.readFileSync(0, "utf8")).harness; + const expectedModel = process.env.PAPERCLIP_AWS_AGENTCORE_MODEL; + if (!h || h.model?.bedrockModelConfig?.modelId !== expectedModel) throw new Error("AgentCore model drift"); + if (JSON.stringify(h.allowedTools) !== JSON.stringify(["@*/pc_*"])) throw new Error("AgentCore tool allowlist drift"); + if (!Array.isArray(h.tools) || h.tools.length !== 0) throw new Error("AgentCore persistent tool drift"); + if (!Array.isArray(h.skills) || h.skills.length !== 0) throw new Error("AgentCore skill drift"); + ' aws --region "$AWS_REGION_NAME" --no-cli-pager bedrock-agentcore-control get-harness-endpoint \ --harness-id "$PAPERCLIP_AWS_AGENTCORE_HARNESS_ID" \ --endpoint-name "$PAPERCLIP_AWS_AGENTCORE_ENDPOINT_QUALIFIER" >/dev/null @@ -245,9 +266,11 @@ provision() { --capabilities CAPABILITY_NAMED_IAM \ --parameter-overrides \ "EnvironmentName=development" "DeploymentMode=$DEPLOYMENT_MODE" \ + "HarnessEndpointName=$ENDPOINT_NAME" \ "TrustedRunnerPrincipalArn=$TRUSTED_PRINCIPAL" "BedrockModelId=$MODEL_ID" \ "BedrockModelResourceArn=$model_resource_arn" \ "BedrockFoundationModelResourceArn=$foundation_model_resource_arn" \ + "BedrockMarketplaceProductId=$MARKETPLACE_PRODUCT_ID" \ --tags paperclip:owned=true paperclip:environment=development paperclip:cost-center=runner-lab local harness_id harness_arn harness_version runtime_arn memory_id memory_arn role_arn harness_id="$(stack_output HarnessId)" @@ -272,7 +295,7 @@ provision() { wait_for_endpoint_status "$harness_id" "$ENDPOINT_NAME" local endpoint_json endpoint_arn endpoint_json="$(aws_cli bedrock-agentcore-control get-harness-endpoint --harness-id "$harness_id" --endpoint-name "$ENDPOINT_NAME" --output json)" - endpoint_arn="$(printf '%s' "$endpoint_json" | node -e 'const fs=require("fs");const o=JSON.parse(fs.readFileSync(0,"utf8"));process.stdout.write(o.harnessEndpointArn||o.endpointArn||o.arn||"")')" + endpoint_arn="$(printf '%s' "$endpoint_json" | node -e 'const fs=require("fs");const o=JSON.parse(fs.readFileSync(0,"utf8"));process.stdout.write(o.endpoint?.arn||o.harnessEndpointArn||o.endpointArn||o.arn||"")')" [[ -n "$endpoint_arn" ]] || endpoint_arn="$harness_arn/endpoint/$ENDPOINT_NAME" mkdir -p "$LOCAL_DIR" umask 077 @@ -296,6 +319,9 @@ provision() { "PAPERCLIP_AWS_AGENTCORE_MODEL=$MODEL_ID" \ "PAPERCLIP_AWS_AGENTCORE_QUALIFICATION_REVISION=aws-agentcore-harness-v1" \ "PAPERCLIP_AWS_AGENTCORE_EVENT_EXPIRY_DAYS=90" >"$tmp" + if [[ -n "${AWS_CONFIG_FILE:-}" ]]; then + printf 'AWS_CONFIG_FILE=%s\n' "$AWS_CONFIG_FILE" >>"$tmp" + fi chmod 600 "$tmp" mv "$tmp" "$ENV_FILE" probe diff --git a/packages/paperclip-runner/scripts/capability-aws-agentcore-smoke.mjs b/packages/paperclip-runner/scripts/capability-aws-agentcore-smoke.mjs index 065f77134ae..ae532c94c74 100644 --- a/packages/paperclip-runner/scripts/capability-aws-agentcore-smoke.mjs +++ b/packages/paperclip-runner/scripts/capability-aws-agentcore-smoke.mjs @@ -106,7 +106,15 @@ async function main() { assert(read.toolResult && typeof read.toolResult === "object", "the mock Paperclip read operation succeeded"); const serialized = JSON.stringify({ opened, first, second, read }); - for (const pattern of [/AWS_ACCESS_KEY_ID/i, /AWS_SECRET_ACCESS_KEY/i, /AWS_SESSION_TOKEN/i, /X-Amz-Signature/i, /Authorization/i]) { + for (const pattern of [ + /AWS_ACCESS_KEY_ID/i, + /AWS_SECRET_ACCESS_KEY/i, + /AWS_SESSION_TOKEN/i, + /X-Amz-Signature/i, + /AWS4-HMAC-SHA256/i, + /"Authorization"\s*:/, + /"Proxy-Authorization"\s*:/, + ]) { assert(!pattern.test(serialized), `browser payload excludes ${pattern}`); } process.stdout.write(`${JSON.stringify({ diff --git a/packages/paperclip-runner/scripts/check-clean-consumers.mjs b/packages/paperclip-runner/scripts/check-clean-consumers.mjs index 337d6161195..cf2e18148a1 100644 --- a/packages/paperclip-runner/scripts/check-clean-consumers.mjs +++ b/packages/paperclip-runner/scripts/check-clean-consumers.mjs @@ -16,6 +16,7 @@ const artifactsRoot = resolve(scratchRoot, "artifacts"); const publicationRoot = process.env.PAPERCLIP_CLEAN_CONSUMER_OUTPUT_DIR === undefined ? undefined : resolve(process.env.PAPERCLIP_CLEAN_CONSUMER_OUTPUT_DIR); +const pnpmInvocation = resolvePnpmInvocation(); await mkdir(artifactsRoot, { recursive: true }); try { @@ -92,7 +93,10 @@ try { async function pack(packageRoot, destination) { const before = new Set(await readdir(destination)); - run("pnpm", ["pack", "--pack-destination", destination], packageRoot, { quiet: true }); + // Every package was already built in the workspace. Ignore package-local + // prepack scripts here so installed, patched dependencies can be archived + // without depending on their unpublished development toolchains. + run("npm", ["pack", "--ignore-scripts", "--pack-destination", destination], packageRoot, { quiet: true }); const created = (await readdir(destination)) .filter((entry) => entry.endsWith(".tgz") && !before.has(entry)) .sort(); @@ -103,21 +107,67 @@ async function pack(packageRoot, destination) { } async function packRunnerRuntimeDependencies(destination) { - const ajvRoot = resolve(runnerRoot, "node_modules/ajv"); - const ajvModules = dirname(await realpath(ajvRoot)); - const packageNames = [ - "ajv", - "fast-deep-equal", - "fast-uri", - "json-schema-traverse", - "require-from-string", - ]; - const tarballs = {}; - for (const packageName of packageNames) { - const packageRoot = packageName === "ajv" ? ajvRoot : resolve(ajvModules, packageName); - tarballs[packageName] = await pack(packageRoot, destination); + const runnerManifest = JSON.parse(await readFile(resolve(runnerRoot, "package.json"), "utf8")); + const overrides = {}; + const packed = new Map(); + const queued = new Set(); + const queue = []; + + const enqueue = async (packageRoot, overrideKey) => { + const concreteRoot = await realpath(packageRoot); + const manifest = JSON.parse(await readFile(resolve(concreteRoot, "package.json"), "utf8")); + if (typeof manifest.name !== "string" || typeof manifest.version !== "string") { + throw new Error(`Runtime dependency at ${concreteRoot} has no exact package identity`); + } + const identity = `${manifest.name}@${manifest.version}`; + let tarball = packed.get(identity); + if (tarball === undefined) { + tarball = await pack(concreteRoot, destination); + packed.set(identity, tarball); + } + overrides[overrideKey] = tarball; + if (!queued.has(identity)) { + queued.add(identity); + queue.push({ root: concreteRoot, manifest }); + } + }; + + for (const packageName of Object.keys(runnerManifest.dependencies ?? {}).sort()) { + await enqueue(resolve(runnerRoot, "node_modules", packageName), packageName); + } + while (queue.length > 0) { + const current = queue.shift(); + const required = current.manifest.dependencies ?? {}; + const optional = current.manifest.optionalDependencies ?? {}; + const peers = current.manifest.peerDependencies ?? {}; + const optionalPeers = current.manifest.peerDependenciesMeta ?? {}; + for (const dependencyName of Object.keys({ ...required, ...optional, ...peers }).sort()) { + const dependencyRoot = await resolveInstalledDependencyRoot(current.root, dependencyName); + if (dependencyRoot === null) { + if (dependencyName in optional || optionalPeers[dependencyName]?.optional === true) continue; + throw new Error(`${current.manifest.name}@${current.manifest.version} dependency ${dependencyName} is not installed`); + } + await enqueue( + dependencyRoot, + `${current.manifest.name}@${current.manifest.version}>${dependencyName}`, + ); + } + } + return overrides; +} + +async function resolveInstalledDependencyRoot(packageRoot, dependencyName) { + let cursor = packageRoot; + while (true) { + const candidate = resolve(cursor, "node_modules", dependencyName); + try { + return await realpath(candidate); + } catch { + const parent = dirname(cursor); + if (parent === cursor) return null; + cursor = parent; + } } - return tarballs; } async function stageRunnerdArtifact(destination) { @@ -136,7 +186,7 @@ async function stageRunnerdArtifact(destination) { function localOverrides(tarballs) { return Object.fromEntries( - Object.entries(tarballs).map(([packageName, tarball]) => [packageName, `file:${tarball}`]), + Object.entries(tarballs).map(([selector, tarball]) => [selector, `file:${tarball}`]), ); } @@ -436,10 +486,8 @@ function installAndRun(consumerRoot, extraEnv = {}) { function run(command, args, cwd, { quiet = false, env = {} } = {}) { const usesPnpm = command === "pnpm"; - const executable = usesPnpm - ? (process.platform === "win32" ? "corepack.cmd" : "corepack") - : command; - const effectiveArgs = usesPnpm ? ["pnpm@9.15.4", ...args] : args; + const executable = usesPnpm ? pnpmInvocation.executable : command; + const effectiveArgs = usesPnpm ? [...pnpmInvocation.prefixArgs, ...args] : args; const result = spawnSync(executable, effectiveArgs, { cwd, encoding: "utf8", @@ -458,6 +506,29 @@ function run(command, args, cwd, { quiet = false, env = {} } = {}) { } } +function resolvePnpmInvocation() { + const corepack = process.platform === "win32" ? "corepack.cmd" : "corepack"; + const corepackProbe = spawnSync(corepack, ["pnpm@9.15.4", "--version"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (corepackProbe.status === 0 && corepackProbe.stdout.trim() === "9.15.4") { + return { executable: corepack, prefixArgs: ["pnpm@9.15.4"] }; + } + + const direct = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; + const directProbe = spawnSync(direct, ["--version"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (directProbe.status === 0 && directProbe.stdout.trim() === "9.15.4") { + return { executable: direct, prefixArgs: [] }; + } + throw new Error( + "Clean-consumer verification requires pnpm 9.15.4 via corepack or the active PATH", + ); +} + function capture(command, args, cwd) { const result = spawnSync(command, args, { cwd, diff --git a/packages/paperclip-runner/spec/native-runner-contract.md b/packages/paperclip-runner/spec/native-runner-contract.md index 81bf353991e..cfe76848d36 100644 --- a/packages/paperclip-runner/spec/native-runner-contract.md +++ b/packages/paperclip-runner/spec/native-runner-contract.md @@ -2205,8 +2205,10 @@ Do not depend on its experimental network WebSocket as the Paperclip WAN transpo 3. Send protocol initialization. 4. Confirm server capabilities/version. 5. Register client-defined semantic tools if configured. -6. Disable automatic skill, app, collaboration, plugin, memory, and - multi-agent instruction injection in thread config. +6. Disable automatic skill, app, plugin, memory, and multi-agent instruction + injection in thread config. Enable Codex's built-in collaboration + instructions by default; permit an explicit eval-only opt-out without + changing the production default. 7. Create or resume thread. 8. emit `session.started` or `session.resumed`. 9. Persist thread/session parameters in the normalized session binding. diff --git a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts index fcdc3cc9720..472d14841a4 100644 --- a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts +++ b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts @@ -3,8 +3,12 @@ import { createHash } from "node:crypto"; import { createInterface } from "node:readline"; import { isAbsolute, relative, resolve } from "node:path"; -import type { AcpPermissionDecision, AcpRuntimeEvent } from "acpx/runtime"; +import type { AcpPermissionDecision, AcpRuntimeEvent, AcpRuntimeStatus } from "acpx/runtime"; +import { + CODEX_BLOCK_TOOL_NAME, + CODEX_COMPLETION_TOOL_NAME, +} from "../contracts/codex.js"; import { AcpxRuntimeHost } from "../drivers/acpx/acpx-runtime-host.js"; import { resolveQualifiedAcpxProfile, type QualifiedAcpxAgent } from "../drivers/acpx/qualified-profiles.js"; import { @@ -19,6 +23,7 @@ import { type AcpxSidecarRequest, type AcpxSidecarResponse, } from "../drivers/acpx/sidecar-protocol.js"; +import { validatePrpStructuredRunResult } from "../protocol/replay-contract.js"; interface PendingResolution { turnId: string; @@ -89,7 +94,12 @@ async function dispatch(request: AcpxSidecarRequest): Promise { + if (!turnId) return; + const { cost, ...breakdown } = usage; + emit("runtime.event", { + type: "status", + text: "Usage updated.", + tag: "usage_update", + used: usage.totalTokens ?? null, + size: null, + cost: cost ?? null, + breakdown, + }); + }, onDiagnostic: (message) => diagnostic("acp_agent_stderr", message), }); } @@ -201,6 +224,21 @@ async function pumpTurn(currentTurnId: string, runtimeTurn: ReturnType { if (!turnId || tools.has(call.callId)) throw new Error("ACPX tool call is unbound or duplicated"); + if (call.tool === CODEX_COMPLETION_TOOL_NAME || call.tool === CODEX_BLOCK_TOOL_NAME) { + const validation = validatePrpStructuredRunResult(call.arguments); + if (!validation.ok) throw new Error("ACPX semantic result failed PRP schema validation"); + if ( + (call.tool === CODEX_BLOCK_TOOL_NAME && validation.result.reportedWorkDisposition !== "blocked") + || (call.tool === CODEX_COMPLETION_TOOL_NAME && validation.result.reportedWorkDisposition === "blocked") + ) throw new Error("ACPX semantic result disposition does not match its terminal operation"); + emit("runtime.event", { + type: "semantic_result", + callId: call.callId, + operationId: call.tool, + result: validation.result, + }); + return Promise.resolve({ accepted: true }); + } emit("runtime.tool_called", { callId: call.callId, operationId: call.tool, @@ -276,6 +314,40 @@ function sanitizeRuntimeEvent(event: AcpRuntimeEvent): Record { return { type: "provider_notice", category: "unclassified_acp_unknown", summary: "The qualified ACP agent emitted an unclassified runtime update." }; } +function sanitizeRuntimeStatus(status: AcpRuntimeStatus): Record { + const cumulative = status.usage?.cumulative; + const cost = status.usage?.cost; + return boundedSidecarValue({ + summary: status.summary ? safeText(status.summary).slice(0, 4000) : null, + acpxRecordId: text(status.acpxRecordId).slice(0, 240) || null, + backendSessionId: text(status.backendSessionId).slice(0, 240) || null, + agentSessionId: text(status.agentSessionId).slice(0, 240) || null, + models: { + currentModelId: text(status.models?.currentModelId).slice(0, 240) || null, + availableModelCount: Math.min(status.models?.availableModelIds.length ?? 0, 100_000), + }, + usage: { + cumulative: cumulative ? { + inputTokens: safeNonNegativeNumber(cumulative.inputTokens), + outputTokens: safeNonNegativeNumber(cumulative.outputTokens), + cachedReadTokens: safeNonNegativeNumber(cumulative.cachedReadTokens), + cachedWriteTokens: safeNonNegativeNumber(cumulative.cachedWriteTokens), + thoughtTokens: safeNonNegativeNumber(cumulative.thoughtTokens), + totalTokens: safeNonNegativeNumber(cumulative.totalTokens), + } : null, + cost: cost ? { + amount: safeNonNegativeNumber(cost.amount), + currency: text(cost.currency).slice(0, 16) || null, + } : null, + requestCount: Math.min(Object.keys(status.usage?.perRequest ?? {}).length, 100_000), + }, + }, 32 * 1024); +} + +function safeNonNegativeNumber(value: unknown): number | null { + return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null; +} + function safeOutput(value: unknown): Record { if (value === undefined || value === null) return { output: null, outputBytes: 0, outputTruncated: false, outputDigest: null }; const raw = typeof value === "string" ? value : JSON.stringify(value); diff --git a/packages/paperclip-runner/src/cli/eval-session.ts b/packages/paperclip-runner/src/cli/eval-session.ts index 28c8da693b3..096909826b4 100644 --- a/packages/paperclip-runner/src/cli/eval-session.ts +++ b/packages/paperclip-runner/src/cli/eval-session.ts @@ -25,10 +25,18 @@ interface EvalSessionRequest { attemptId: string; prompt: string; model: string; - provider?: "codex" | "opencode" | "aws_agentcore" | "acpx"; - driver?: "codex_app_server" | "opencode_server" | "aws_agentcore_harness_api" | "acpx_runtime"; + provider?: "codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx"; + driver?: "codex_app_server" | "opencode_server" | "claude_managed_agents_api" | "aws_agentcore_harness_api" | "acpx_runtime"; opencodeVersion?: string; acpxAgent?: QualifiedAcpxAgent; + managedProfile?: { + profileId: string; + anthropicAgentId: string; + agentVersion: string; + environmentId: string; + betaVersion: "managed-agents-2026-04-01"; + maxSessionListCostUsd: number; + }; agentCoreProfile?: { profileId: string; region: string; @@ -51,6 +59,8 @@ interface EvalSessionRequest { runnerd: { path: string; sha256: string }; limits: { turnTimeoutMs: number; maxAgentTurns: number; maxEstimatedCostNanodollars: number }; session: CreateCapabilityLiveSessionInput; + /** Codex collaboration/tool-preamble instructions are enabled unless explicitly false. */ + includeCollaborationModeInstructions?: boolean; } function argument(name: string): string { @@ -78,6 +88,7 @@ const actualDigest = await sha256(runnerdPath); function infrastructureCategory(failureClass: string): string { if (failureClass === "provider_turn_timeout") return "provider_lifecycle"; + if (failureClass === "provider_budget_reached") return "provider_budget"; if (failureClass === "runner_shutdown_timeout") return "runner_lifecycle"; if (failureClass === "runner_exit_failure") return "runner_infrastructure"; return "eval_orchestration"; @@ -86,6 +97,12 @@ if (actualDigest !== request.runnerd.sha256.replace(/^sha256:/, "")) { throw new Error(`runnerd digest mismatch: expected ${request.runnerd.sha256}, got sha256:${actualDigest}`); } +const requestedProvider = request.provider ?? "codex"; +const requestedDriver = request.driver ?? (request.provider === "opencode" ? "opencode_server" : request.provider === "claude_managed" ? "claude_managed_agents_api" : request.provider === "aws_agentcore" ? "aws_agentcore_harness_api" : request.provider === "acpx" ? "acpx_runtime" : "codex_app_server"); +const requestedProviderVersion = request.provider === "opencode" ? request.opencodeVersion ?? "1.18.17" : request.provider === "claude_managed" ? request.managedProfile?.agentVersion ?? null : request.provider === "aws_agentcore" ? request.agentCoreProfile?.harnessVersion ?? null : request.provider === "acpx" ? acpxProfile!.acpxVersion : null; +let retainedProviderSessionId: string | null = null; +let failedUsage: Record | null = null; + try { const execution = await runDurableEvalSession({ attemptId: request.attemptId, @@ -94,6 +111,7 @@ try { provider: request.provider ?? "codex", acpxAgent: request.acpxAgent, opencodeVersion: request.opencodeVersion, + managedProfile: request.managedProfile, agentCoreProfile: request.agentCoreProfile, runnerBinaryPath: runnerdPath, seed: request.session.seed ?? {}, @@ -103,10 +121,13 @@ try { explicitClaims: request.session.explicitClaims ?? [], turnTimeoutMs: request.limits.turnTimeoutMs, toolExposure: request.session.toolExposure, + includeCollaborationModeInstructions: + request.includeCollaborationModeInstructions, }); const turn = execution.turn as Record; const snapshot = execution.snapshot as Parameters[0]; const reconciled = reconcileCapabilityLiveUsage(snapshot); + retainedProviderSessionId = typeof snapshot.providerSessionId === "string" ? snapshot.providerSessionId : null; if ((snapshot.usageLedger ?? []).length === 0) throw new Error("completed turn omitted usage accounting"); const estimate = estimateModelCostNanodollars(request.model, reconciled); const usage = { @@ -116,19 +137,24 @@ try { outputTokens: reconciled.outputTokens, cachedInputTokens: reconciled.cachedInputTokens, reasoningTokens: reconciled.reasoningTokens, + providerReportedCostNanodollars: reconciled.costNanodollars, ...estimate, }; + failedUsage = usage; if (usage.agentTurns > request.limits.maxAgentTurns) throw new Error("agent turn limit exceeded"); if (usage.estimatedCostNanodollars > request.limits.maxEstimatedCostNanodollars) throw new Error("estimated cost limit exceeded"); + if (usage.providerReportedCostNanodollars > request.limits.maxEstimatedCostNanodollars) throw new Error("provider-reported cost limit exceeded"); await writeFile(outputPath, `${JSON.stringify({ schema: "paperclip-runner/eval-session-artifact/v1", attemptId: request.attemptId, build: PAPERCLIP_RUNNER_BUILD_METADATA, runnerd: { path: "[withheld]", sha256: `sha256:${actualDigest}` }, requestedModel: request.model, - provider: request.provider ?? "codex", - driver: request.driver ?? (request.provider === "opencode" ? "opencode_server" : request.provider === "aws_agentcore" ? "aws_agentcore_harness_api" : request.provider === "acpx" ? "acpx_runtime" : "codex_app_server"), - providerVersion: request.provider === "opencode" ? request.opencodeVersion ?? "1.18.17" : request.provider === "aws_agentcore" ? request.agentCoreProfile?.harnessVersion ?? null : request.provider === "acpx" ? acpxProfile!.acpxVersion : null, + provider: requestedProvider, + driver: requestedDriver, + providerVersion: requestedProviderVersion, + providerSessionId: retainedProviderSessionId, + ...(request.provider === "claude_managed" ? { managedProfile: request.managedProfile, retainedSession: retainedProviderSessionId !== null, retainedSessionStatus: retainedProviderSessionId === null ? "unknown" : "retained" } : {}), ...(acpxProfile === null ? {} : { acpxAgent: acpxProfile.agent, acpxProfile }), turn, snapshot, @@ -151,6 +177,38 @@ try { retryable: false, diagnostics: {}, }; + const failureDiagnostics = infrastructureFailure.diagnostics as Record; + const diagnosticSessionId = typeof failureDiagnostics.providerSessionId === "string" + ? failureDiagnostics.providerSessionId + : null; + retainedProviderSessionId ??= diagnosticSessionId; + const failureRunDelta = failureDiagnostics.usageRunDelta; + if (failedUsage === null && typeof failureRunDelta === "object" && failureRunDelta !== null && !Array.isArray(failureRunDelta)) { + const runDelta = failureRunDelta as Record; + const nonNegativeNumber = (name: string): number => { + const value = runDelta[name]; + return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; + }; + const receipt = { + providerCalls: 1, + providerRequests: nonNegativeNumber("requests"), + inputTokens: nonNegativeNumber("inputTokens"), + outputTokens: nonNegativeNumber("outputTokens"), + cachedInputTokens: nonNegativeNumber("cacheReadTokens"), + reasoningTokens: nonNegativeNumber("reasoningTokens"), + costNanodollars: Math.round(nonNegativeNumber("providerCostUsd") * 1_000_000_000), + }; + failedUsage = { + agentTurns: receipt.providerCalls, + providerRequests: receipt.providerRequests, + inputTokens: receipt.inputTokens, + outputTokens: receipt.outputTokens, + cachedInputTokens: receipt.cachedInputTokens, + reasoningTokens: receipt.reasoningTokens, + providerReportedCostNanodollars: receipt.costNanodollars, + ...estimateModelCostNanodollars(request.model, receipt), + }; + } await writeFile(outputPath, `${JSON.stringify({ schema: "paperclip-runner/eval-session-artifact/v1", attemptId: request.attemptId, @@ -159,9 +217,12 @@ try { build: PAPERCLIP_RUNNER_BUILD_METADATA, runnerd: { path: "[withheld]", sha256: `sha256:${actualDigest}` }, requestedModel: request.model, - provider: request.provider ?? "codex", - driver: request.driver ?? (request.provider === "opencode" ? "opencode_server" : request.provider === "aws_agentcore" ? "aws_agentcore_harness_api" : request.provider === "acpx" ? "acpx_runtime" : "codex_app_server"), - providerVersion: request.provider === "opencode" ? request.opencodeVersion ?? "1.18.17" : request.provider === "aws_agentcore" ? request.agentCoreProfile?.harnessVersion ?? null : request.provider === "acpx" ? acpxProfile!.acpxVersion : null, + provider: requestedProvider, + driver: requestedDriver, + providerVersion: requestedProviderVersion, + providerSessionId: retainedProviderSessionId, + ...(failedUsage === null ? {} : { usage: failedUsage }), + ...(request.provider === "claude_managed" ? { managedProfile: request.managedProfile, retainedSession: retainedProviderSessionId !== null, retainedSessionStatus: retainedProviderSessionId === null ? "unknown" : "retained" } : {}), ...(acpxProfile === null ? {} : { acpxAgent: acpxProfile.agent, acpxProfile }), timing: { startedAt: evalStartedAt, finishedAt: new Date().toISOString(), durationMs: Date.now() - evalStartedAtMs }, }, null, 2)}\n`); diff --git a/packages/paperclip-runner/src/contracts/provider-trace.ts b/packages/paperclip-runner/src/contracts/provider-trace.ts new file mode 100644 index 00000000000..41ec12e4972 --- /dev/null +++ b/packages/paperclip-runner/src/contracts/provider-trace.ts @@ -0,0 +1,29 @@ +/** + * Provider-neutral debug channel. It is deliberately separate from PRP event + * delivery: its sequence and acknowledgement cannot influence run authority, + * replay cursors, or terminal completion. + */ +export type ProviderTraceRecord = + | ({ kind: "frame" } & Record) + | ({ kind: "interpretation" } & Record) + | ({ kind: "trace_status" } & Record); + +export interface ProviderTraceBatch { + traceId: string; + debugSequence: number; + records: ProviderTraceRecord[]; +} + +export interface ProviderTraceAck { + traceId: string; + acknowledgedDebugSequence: number; +} + +export interface ProviderTraceSink { + appendBatch(batch: ProviderTraceBatch): Promise; + finish(input: { + traceId: string; + status: "complete" | "incomplete" | "truncated"; + reason?: string | null; + }): Promise; +} diff --git a/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts b/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts index 482173f7ab8..d4d73c45d19 100644 --- a/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts +++ b/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts @@ -41,6 +41,7 @@ import { type QualifiedAcpxAgent, } from "../drivers/acpx/qualified-profiles.js"; import { createSanitizedAcpxEnvironment } from "../drivers/acpx/environment.js"; +import { createSanitizedClaudeManagedEnvironment } from "../drivers/claude-managed/environment.js"; import { DURABLE_RECOVERY_FAULTS, @@ -79,6 +80,10 @@ const coreStateSchema = "paperclip.runner.durable.control-plane-state.v1"; const maxFrameBytes = 1024 * 1024; const authChallengeTtlMs = 5_000; +function durableEvalConnectionLeaseTtlMs(turnTimeoutMs: number): number { + return Math.max(60 * 60 * 1_000, turnTimeoutMs + 60_000); +} + interface BootstrapTicketRecord { recordId: string; credentialId: string; @@ -185,7 +190,11 @@ export interface DurablePrpControlPlaneOptions { fault?: DurableRecoveryFault; expectedRunnerVersion?: string; expectedRunnerDigest?: string; - onSemanticToolInput?: (input: { callId: string; operationId: string; input: unknown }) => Promise; + onSemanticToolInput?: (input: { + callId: string; + operationId: string; + input: unknown; + }) => Promise; connectionLeaseTtlMs?: number; } @@ -202,7 +211,9 @@ export interface RunnerProcessHandle { } function domainDigest(domain: string, parts: readonly Buffer[]): Buffer { - const digest = createHash("sha256").update(domain).update(Buffer.from([0])); + const digest = createHash("sha256") + .update(domain) + .update(Buffer.from([0])); for (const part of parts) { const length = Buffer.alloc(8); length.writeBigUInt64BE(BigInt(part.length)); @@ -211,8 +222,14 @@ function domainDigest(domain: string, parts: readonly Buffer[]): Buffer { return digest.digest(); } -function domainHmac(key: Buffer, domain: string, parts: readonly Buffer[]): Buffer { - const digest = createHmac("sha256", key).update(domain).update(Buffer.from([0])); +function domainHmac( + key: Buffer, + domain: string, + parts: readonly Buffer[], +): Buffer { + const digest = createHmac("sha256", key) + .update(domain) + .update(Buffer.from([0])); for (const part of parts) { const length = Buffer.alloc(8); length.writeBigUInt64BE(BigInt(part.length)); @@ -221,7 +238,10 @@ function domainHmac(key: Buffer, domain: string, parts: readonly Buffer[]): Buff return digest.digest(); } -function credentialMaterial(token: string): { credentialId: string; authKey: Buffer } { +function credentialMaterial(token: string): { + credentialId: string; + authKey: Buffer; +} { const bytes = Buffer.from(token); return { credentialId: `sha256:${domainDigest("paperclip-runner-credential-id-v1", [bytes]).toString("hex")}`, @@ -253,12 +273,14 @@ function safeDate(offsetMs: number): string { function authKeyFromDigest(digest: string): Buffer { const hex = digest.match(/^sha256:([0-9a-f]{64})$/)?.[1]; - if (hex === undefined) throw new Error("Stored transport authentication key is malformed."); + if (hex === undefined) + throw new Error("Stored transport authentication key is malformed."); return Buffer.from(hex, "hex"); } function proofMatches(expected: Buffer, supplied: unknown): boolean { - if (typeof supplied !== "string" || !/^[0-9a-f]{64}$/.test(supplied)) return false; + if (typeof supplied !== "string" || !/^[0-9a-f]{64}$/.test(supplied)) + return false; return timingSafeEqual(expected, Buffer.from(supplied, "hex")); } @@ -275,8 +297,12 @@ function createSecureChannel( ]; const binding = domainDigest("paperclip-runner-session-binding-v1", parts); return { - sendKey: domainHmac(authKey, "paperclip-runner-core-to-client-key-v1", [binding]), - receiveKey: domainHmac(authKey, "paperclip-runner-client-to-core-key-v1", [binding]), + sendKey: domainHmac(authKey, "paperclip-runner-core-to-client-key-v1", [ + binding, + ]), + receiveKey: domainHmac(authKey, "paperclip-runner-client-to-core-key-v1", [ + binding, + ]), sendCounter: 0n, receiveCounter: 0n, sessionId: `sha256:${binding.toString("hex")}`, @@ -295,12 +321,21 @@ function secureAad( direction: "client_to_core" | "core_to_client", counter: bigint, ): Buffer { - return Buffer.from(`${secureFrameSchema}\0${channel.sessionId}\0${direction}\0${counter}`); + return Buffer.from( + `${secureFrameSchema}\0${channel.sessionId}\0${direction}\0${counter}`, + ); } -function encryptSecureJson(channel: SecureChannel, value: unknown): Record { +function encryptSecureJson( + channel: SecureChannel, + value: unknown, +): Record { const counter = channel.sendCounter; - const cipher = createCipheriv("aes-256-gcm", channel.sendKey, secureNonce("P3S1", counter)); + const cipher = createCipheriv( + "aes-256-gcm", + channel.sendKey, + secureNonce("P3S1", counter), + ); cipher.setAAD(secureAad(channel, "core_to_client", counter)); const ciphertext = Buffer.concat([ cipher.update(Buffer.from(JSON.stringify(value))), @@ -315,7 +350,10 @@ function encryptSecureJson(channel: SecureChannel, value: unknown): Record { +function decryptSecureJson( + channel: SecureChannel, + value: unknown, +): Record { if (typeof value !== "object" || value === null) { throw new Error("Secure frame must be an object."); } @@ -332,7 +370,8 @@ function decryptSecureJson(channel: SecureChannel, value: unknown): Record; } @@ -379,10 +421,14 @@ function verifyPrivateDirectory(path: string): void { } if (process.platform !== "win32") { if ((metadata.mode & 0o777) !== 0o700) { - throw new Error(`Private state directory does not use mode 0700: ${path}`); + throw new Error( + `Private state directory does not use mode 0700: ${path}`, + ); } if (process.geteuid !== undefined && metadata.uid !== process.geteuid()) { - throw new Error(`Private state directory is not owned by the daemon user: ${path}`); + throw new Error( + `Private state directory is not owned by the daemon user: ${path}`, + ); } } } @@ -396,7 +442,9 @@ function verifyPrivateRegularFile(file: Stats, path: string): void { throw new Error(`Private state file does not use mode 0600: ${path}`); } if (process.geteuid !== undefined && file.uid !== process.geteuid()) { - throw new Error(`Private state file is not owned by the daemon user: ${path}`); + throw new Error( + `Private state file is not owned by the daemon user: ${path}`, + ); } } } @@ -404,7 +452,10 @@ function verifyPrivateRegularFile(file: Stats, path: string): void { function readPrivateFile(path: string): string | null { let descriptor: number; try { - descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); + descriptor = openSync( + path, + constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0), + ); } catch (error) { if (isNodeError(error, "ENOENT")) return null; throw error; @@ -421,7 +472,9 @@ function syncParentDirectory(path: string): void { if (process.platform === "win32") return; const descriptor = openSync( dirname(path), - constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0), + constants.O_RDONLY | + (constants.O_DIRECTORY ?? 0) | + (constants.O_NOFOLLOW ?? 0), ); try { fsyncSync(descriptor); @@ -431,7 +484,10 @@ function syncParentDirectory(path: string): void { } function atomicPrivateWrite(path: string, contents: string): void { - const temporary = resolve(dirname(path), `.${path.split(/[\\/]/).at(-1)}.${randomUUID()}.tmp`); + const temporary = resolve( + dirname(path), + `.${path.split(/[\\/]/).at(-1)}.${randomUUID()}.tmp`, + ); let descriptor: number | null = null; let created = false; try { @@ -473,7 +529,9 @@ class DurableCoreStore { try { const metadata = lstatSync(directory); if (metadata.isSymbolicLink() || !metadata.isDirectory()) { - throw new Error(`Private state directory is not a real directory: ${directory}`); + throw new Error( + `Private state directory is not a real directory: ${directory}`, + ); } } catch (error) { if (!isNodeError(error, "ENOENT")) throw error; @@ -489,7 +547,9 @@ class DurableCoreStore { this.#state.schema !== coreStateSchema || canonicalJson(this.#state.identity) !== canonicalJson(identity) ) { - throw new Error("Mock core state does not match the requested Durable recovery identity."); + throw new Error( + "Mock core state does not match the requested Durable recovery identity.", + ); } } else { this.#state = initialCoreState(identity); @@ -540,7 +600,10 @@ class MockWebSocketConnection { } sendJson(value: unknown): void { - const wire = this.secureChannel === null ? value : encryptSecureJson(this.secureChannel, value); + const wire = + this.secureChannel === null + ? value + : encryptSecureJson(this.secureChannel, value); this.sendText(JSON.stringify(wire)); } @@ -603,7 +666,9 @@ class MockWebSocketConnection { if (this.#buffer.length < cursor + 4 + length) return; const mask = this.#buffer.subarray(cursor, cursor + 4); cursor += 4; - const payload = Buffer.from(this.#buffer.subarray(cursor, cursor + length)); + const payload = Buffer.from( + this.#buffer.subarray(cursor, cursor + length), + ); this.#buffer = this.#buffer.subarray(cursor + length); for (let index = 0; index < payload.length; index += 1) { payload[index] = payload[index]! ^ mask[index % 4]!; @@ -624,7 +689,9 @@ class MockWebSocketConnection { #sendControl(opcode: number, payload: Buffer): void { if (payload.length > 125 || this.#closed) return; - this.socket.write(Buffer.concat([Buffer.from([0x80 | opcode, payload.length]), payload])); + this.socket.write( + Buffer.concat([Buffer.from([0x80 | opcode, payload.length]), payload]), + ); } } @@ -677,7 +744,9 @@ export class DurablePrpControlPlane { response.writeHead(404).end(); }); this.#server = server; - server.on("upgrade", (request, socket, head) => this.handleUpgrade(request, socket, "/durableRecovery/connect", head)); + server.on("upgrade", (request, socket, head) => + this.handleUpgrade(request, socket, "/durableRecovery/connect", head), + ); await new Promise((resolveListen, rejectListen) => { server.once("error", rejectListen); server.listen(port, "127.0.0.1", () => { @@ -701,7 +770,9 @@ export class DurablePrpControlPlane { this.#server = null; this.#port = null; if (server !== null) { - await new Promise((resolveClose) => server.close(() => resolveClose())); + await new Promise((resolveClose) => + server.close(() => resolveClose()), + ); } } @@ -713,7 +784,9 @@ export class DurablePrpControlPlane { } activeRunnerConnectionCount(): number { - return [...this.#connections].filter((connection) => connection.secureChannel !== null).length; + return [...this.#connections].filter( + (connection) => connection.secureChannel !== null, + ).length; } issueBootstrapTicket(ttlMs = 5_000): string { @@ -745,7 +818,9 @@ export class DurablePrpControlPlane { const controllerSeq = this.store.state.commands.length + 1; const command: DurableRecoveryCoreCommand = { schema: "paperclip.prp.command.v1", - commandId: commandId ?? `command_durableRecovery_${controllerSeq.toString().padStart(3, "0")}`, + commandId: + commandId ?? + `command_durableRecovery_${controllerSeq.toString().padStart(3, "0")}`, controllerSeq, type, issuedAt: `2026-08-07T23:30:${controllerSeq.toString().padStart(2, "0")}.000Z`, @@ -783,7 +858,8 @@ export class DurablePrpControlPlane { expectedPath = "/api/runner/v1/connect", head: Buffer = Buffer.alloc(0), ): void { - const requestPath = new URL(request.url ?? "/", "http://paperclip.invalid").pathname; + const requestPath = new URL(request.url ?? "/", "http://paperclip.invalid") + .pathname; if (requestPath !== expectedPath) { socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"); socket.destroy(); @@ -830,7 +906,10 @@ export class DurablePrpControlPlane { connection.close(); return; } - if (envelope.protocol !== protocol || envelope.version !== protocolVersion) { + if ( + envelope.protocol !== protocol || + envelope.version !== protocolVersion + ) { connection.close(); return; } @@ -860,7 +939,9 @@ export class DurablePrpControlPlane { } } - #authorizeHello(payload: Record): PendingAuthorization | null { + #authorizeHello( + payload: Record, + ): PendingAuthorization | null { const credentialId = payload.credentialId; if (typeof credentialId !== "string") return null; const ticket = this.store.state.tickets[credentialId]; @@ -918,7 +999,8 @@ export class DurablePrpControlPlane { (authorization.kind === "bootstrap" && (authorization.runnerVersion !== this.#expectedRunnerVersion || authorization.runnerDigest !== this.#expectedRunnerDigest)) || - (authorization.kind === "lease" && authorization.protocolVersion !== protocolVersion) + (authorization.kind === "lease" && + authorization.protocolVersion !== protocolVersion) ) { return null; } @@ -968,7 +1050,10 @@ export class DurablePrpControlPlane { }; } - #authHello(connection: MockWebSocketConnection, envelope: Record): void { + #authHello( + connection: MockWebSocketConnection, + envelope: Record, + ): void { if (connection.pendingChallenge !== null) { connection.close(); return; @@ -998,10 +1083,12 @@ export class DurablePrpControlPlane { runnerVersion: payload.runnerVersion, runnerDigest: payload.runnerDigest, selectedVersion: protocolVersion, - credentialLeaseId: authorization.kind === "lease" ? authorization.leaseId : null, + credentialLeaseId: + authorization.kind === "lease" ? authorization.leaseId : null, credentialExpiresAt: authorization.expiresAt, credentialExpiresAtUnixMs: authorization.expiresAtUnixMs, - revocationEpoch: authorization.kind === "lease" ? authorization.revocationEpoch : 0, + revocationEpoch: + authorization.kind === "lease" ? authorization.revocationEpoch : 0, }; const canonicalChallenge = canonicalJson(challengePayload); const serverProof = domainHmac( @@ -1028,7 +1115,10 @@ export class DurablePrpControlPlane { }); } - #authResponse(connection: MockWebSocketConnection, envelope: Record): void { + #authResponse( + connection: MockWebSocketConnection, + envelope: Record, + ): void { const pending = connection.pendingChallenge; const payload = envelope.payload as Record | undefined; if ( @@ -1044,7 +1134,10 @@ export class DurablePrpControlPlane { // WebSocket callbacks run synchronously on the mock core's event loop. Re-reading, // validating, consuming, minting, and persisting here forms one state mutation // boundary, so another proof cannot interleave with bootstrap consumption. - const authorization = this.#reauthorizePendingChallenge(pending, Date.now()); + const authorization = this.#reauthorizePendingChallenge( + pending, + Date.now(), + ); if (authorization === null) { connection.close(); return; @@ -1052,7 +1145,10 @@ export class DurablePrpControlPlane { const expectedClientProof = domainHmac( authorization.authKey, "paperclip-runner-client-proof-v1", - [Buffer.from(pending.canonicalChallenge), Buffer.from(pending.serverProof)], + [ + Buffer.from(pending.canonicalChallenge), + Buffer.from(pending.serverProof), + ], ); if (!proofMatches(expectedClientProof, payload.clientProof)) { connection.close(); @@ -1065,7 +1161,10 @@ export class DurablePrpControlPlane { authorization.ticket.usedAt = new Date().toISOString(); leaseToken = `lease_${randomUUID()}`; const material = credentialMaterial(leaseToken); - const ttlMs = this.fault === "lease-expiry" && !this.#faultTriggered ? 50 : this.#connectionLeaseTtlMs; + const ttlMs = + this.fault === "lease-expiry" && !this.#faultTriggered + ? 50 + : this.#connectionLeaseTtlMs; const expiresAtUnixMs = Date.now() + ttlMs; lease = { recordId: `connection_lease_record_${randomUUID()}`, @@ -1096,7 +1195,10 @@ export class DurablePrpControlPlane { this.#welcome(connection, leaseToken); } - #welcome(connection: MockWebSocketConnection, leaseToken: string | null): void { + #welcome( + connection: MockWebSocketConnection, + leaseToken: string | null, + ): void { const lease = connection.lease; if (lease === null || connection.connectionId === null) { connection.close(); @@ -1173,13 +1275,17 @@ export class DurablePrpControlPlane { } } - #wireCommand(command: DurableRecoveryCoreCommand): Omit { + #wireCommand( + command: DurableRecoveryCoreCommand, + ): Omit { const { status: _status, result: _result, ...wire } = command; return wire; } #nextPendingCommand(): DurableRecoveryCoreCommand[] { - const command = this.store.state.commands.find((candidate) => candidate.status === "pending"); + const command = this.store.state.commands.find( + (candidate) => candidate.status === "pending", + ); return command === undefined ? [] : [command]; } @@ -1190,7 +1296,9 @@ export class DurablePrpControlPlane { payload: Record, ): Record { if (connection.lease === null || connection.connectionId === null) { - throw new Error("Cannot send control data before transport authentication."); + throw new Error( + "Cannot send control data before transport authentication.", + ); } return { protocol, @@ -1236,7 +1344,9 @@ export class DurablePrpControlPlane { connection.close(); return; } - const command = this.store.state.commands.find((candidate) => candidate.commandId === commandId); + const command = this.store.state.commands.find( + (candidate) => candidate.commandId === commandId, + ); if (command === undefined) { connection.close(); return; @@ -1272,7 +1382,11 @@ export class DurablePrpControlPlane { const runId = payload.runId; const turnId = payload.turnId; const itemId = payload.itemId; - if (typeof runId !== "string" || typeof turnId !== "string" || typeof itemId !== "string") { + if ( + typeof runId !== "string" || + typeof turnId !== "string" || + typeof itemId !== "string" + ) { throw new Error("completed run.attach omitted its identity binding"); } const next = { ...this.identity, runId, turnId, itemId }; @@ -1286,7 +1400,10 @@ export class DurablePrpControlPlane { } } - #event(connection: MockWebSocketConnection, envelope: Record): void { + #event( + connection: MockWebSocketConnection, + envelope: Record, + ): void { const event = envelope.payload as Record | undefined; const sourceSeq = event?.sourceSeq; const sourceEventId = event?.sourceEventId; @@ -1335,25 +1452,48 @@ export class DurablePrpControlPlane { } this.store.save(); - const semantic = (event.payload as Record | undefined)?.semantic_tool as Record | undefined; + const semantic = (event.payload as Record | undefined) + ?.semantic_tool as Record | undefined; if ( - newlyCommitted && (eventType === "semantic_tool.input" || eventType === "mcp_app.tool_input") && this.#onSemanticToolInput && semantic?.phase === "input" && - typeof semantic.callId === "string" && typeof semantic.operationId === "string" + newlyCommitted && + (eventType === "semantic_tool.input" || + eventType === "mcp_app.tool_input") && + this.#onSemanticToolInput && + semantic?.phase === "input" && + typeof semantic.callId === "string" && + typeof semantic.operationId === "string" ) { - const call = { callId: semantic.callId, operationId: semantic.operationId, input: semantic.input }; + const call = { + callId: semantic.callId, + operationId: semantic.operationId, + input: semantic.input, + }; void this.#onSemanticToolInput(call).then((value) => { - const outcome = typeof value === "object" && value !== null && !Array.isArray(value) - ? value as Record - : {}; + const outcome = + typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : {}; const wrapped = outcome.__paperclipSemanticToolOutcome === true; const result = wrapped ? outcome.result : value; const isError = wrapped && outcome.isError === true; - const runScope = createHash("sha256").update(this.identity.runId).digest("hex").slice(0, 12); - this.queueCommand("semantic_tool.result", { ...call, result, isError }, `command_tool_${runScope}_${call.callId}`, true); + const runScope = createHash("sha256") + .update(this.identity.runId) + .digest("hex") + .slice(0, 12); + this.queueCommand( + "semantic_tool.result", + { ...call, result, isError }, + `command_tool_${runScope}_${call.callId}`, + true, + ); }); } - if (this.fault === "revoke" && eventType === "run.terminal" && !this.#faultTriggered) { + if ( + this.fault === "revoke" && + eventType === "run.terminal" && + !this.#faultTriggered + ) { if (connection.lease !== null) { connection.lease.revokedAt = new Date().toISOString(); connection.lease.revocationEpoch += 1; @@ -1378,7 +1518,8 @@ export class DurablePrpControlPlane { } const shouldLoseAck = - !this.#faultTriggered && (this.fault === "lost-ack" || this.fault === "runner-restart"); + !this.#faultTriggered && + (this.fault === "lost-ack" || this.fault === "runner-restart"); if (shouldLoseAck) { this.#replayCursorOverrideOnce = true; this.#triggerFault(); @@ -1389,23 +1530,66 @@ export class DurablePrpControlPlane { } connection.sendJson( - this.#controlEnvelope(connection, `ack_${this.store.state.ackedSourceSeq}`, "ack", { - ackedSourceSeq: this.store.state.ackedSourceSeq, - }), + this.#controlEnvelope( + connection, + `ack_${this.store.state.ackedSourceSeq}`, + "ack", + { + ackedSourceSeq: this.store.state.ackedSourceSeq, + }, + ), ); } } -function runnerEnvironment(ticket: string, source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { +function runnerEnvironment( + ticket: string, + source: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { const environment: NodeJS.ProcessEnv = { PAPERCLIP_RUNNER_BOOTSTRAP_TICKET: ticket, }; for (const key of [ - "PATH", "SystemRoot", "WINDIR", "PATHEXT", "LANG", "LC_ALL", "TZ", - "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "ALL_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", - "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "RUST_BACKTRACE", - "PAPERCLIP_OPENCODE_COMMAND", "PAPERCLIP_OPENCODE_RUNTIME_DIR", - "PAPERCLIP_RUNNER_INSTANCE_ID", "PAPERCLIP_RUN_ID", "PAPERCLIP_NORMALIZED_SESSION_ID", + "PATH", + "HOME", + "SystemRoot", + "WINDIR", + "PATHEXT", + "LANG", + "LC_ALL", + "LC_CTYPE", + "TZ", + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", + "ALL_PROXY", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "OPENROUTER_API_KEY", + "ANTHROPIC_API_KEY", + "RUST_BACKTRACE", + // AgentCore resolves an AWS profile or workload identity inside runnerd, + // then assumes the immutable invocation role. These are locator/config + // values, not exported access keys or session credentials. + "AWS_PROFILE", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_CONFIG_FILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_ROLE_ARN", + "AWS_ROLE_SESSION_NAME", + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "PAPERCLIP_OPENCODE_COMMAND", + "PAPERCLIP_OPENCODE_RUNTIME_DIR", + "PAPERCLIP_RUNNER_INSTANCE_ID", + "PAPERCLIP_RUN_ID", + "PAPERCLIP_NORMALIZED_SESSION_ID", + // Short-lived, controller-selected sidecar capture. These never enter the + // canonical PRP environment or provider payload; runnerd alone consumes them. + "PAPERCLIP_PROVIDER_TRACE_PATH", + "PAPERCLIP_PROVIDER_TRACE_MAX_BYTES", ]) { const value = source[key]; if (value !== undefined) environment[key] = value; @@ -1468,7 +1652,10 @@ export function spawnRunner(options: { if (options.lifecyclePolicy !== undefined) { args.push("--lifecycle-mode", options.lifecyclePolicy.mode); if (options.lifecyclePolicy.mode === "warm") { - args.push("--idle-timeout-ms", String(options.lifecyclePolicy.idleTimeoutMs)); + args.push( + "--idle-timeout-ms", + String(options.lifecyclePolicy.idleTimeoutMs), + ); } } const child = spawn(options.runnerBinaryPath ?? runnerBinary, args, { @@ -1484,10 +1671,14 @@ export function spawnRunner(options: { child.stderr.setEncoding("utf8").on("data", (chunk: string) => { stderr = `${stderr}${chunk}`.slice(-16_384); }); - const completion = new Promise((resolveCompletion, rejectCompletion) => { - child.once("error", rejectCompletion); - child.once("exit", (code, signal) => resolveCompletion({ code, signal, stdout, stderr })); - }); + const completion = new Promise( + (resolveCompletion, rejectCompletion) => { + child.once("error", rejectCompletion); + child.once("exit", (code, signal) => + resolveCompletion({ code, signal, stdout, stderr }), + ); + }, + ); return { child, completion }; } @@ -1513,7 +1704,9 @@ export async function waitForProcess( /** Test-fixture compatibility wrapper that requires an explicit injected fault mode. */ export class DurableRecoveryMockCore extends DurablePrpControlPlane { - constructor(options: DurablePrpControlPlaneOptions & { fault: DurableRecoveryFault }) { + constructor( + options: DurablePrpControlPlaneOptions & { fault: DurableRecoveryFault }, + ) { super(options); } } @@ -1529,8 +1722,13 @@ function durableRecoveryIdentity(): DurableRecoveryIdentity { }; } -function queueScenario(core: DurableRecoveryMockCore, fault: DurableRecoveryFault): void { - core.queueCommand("run.prepare", { workspace: "durable-recovery-durable-fixture" }); +function queueScenario( + core: DurableRecoveryMockCore, + fault: DurableRecoveryFault, +): void { + core.queueCommand("run.prepare", { + workspace: "durable-recovery-durable-fixture", + }); core.queueCommand("session.open", { reuse: "same_session" }); if (fault === "harness-restart") { core.queueCommand("fault.harness_restart", {}); @@ -1556,13 +1754,18 @@ function readRunnerState(stateDirectory: string): DurableRecoveryRunnerState { ) as DurableRecoveryRunnerState; } -function assertContinuous(events: readonly DurableRecoveryCommittedEvent[]): boolean { +function assertContinuous( + events: readonly DurableRecoveryCommittedEvent[], +): boolean { return events .toSorted((left, right) => left.sourceSeq - right.sourceSeq) .every((event, index) => event.sourceSeq === index + 1); } -function secretLeakCount(paths: readonly string[], secrets: readonly string[]): number { +function secretLeakCount( + paths: readonly string[], + secrets: readonly string[], +): number { let leaks = 0; for (const path of paths) { const source = readFileSync(path, "utf8"); @@ -1573,7 +1776,10 @@ function secretLeakCount(paths: readonly string[], secrets: readonly string[]): return leaks; } -function persistedCapabilityShape(path: string, fieldNames: readonly string[]): boolean { +function persistedCapabilityShape( + path: string, + fieldNames: readonly string[], +): boolean { const source = readFileSync(path, "utf8"); return fieldNames.some((fieldName) => source.includes(`\"${fieldName}\"`)); } @@ -1607,32 +1813,71 @@ export async function runDurableToolBridgeConformance( const runnerStateDirectory = resolve(root, "runner"); mkdirSync(runnerStateDirectory, { recursive: true, mode: 0o700 }); const authority = new CapabilityMockControlPlaneAdapter({ - actors: [{ id: "actor-1", companyId: "company-1", name: "Bridge agent", role: "engineer", status: "active", budgetId: "budget-1", capabilityGrants: [] }], + actors: [ + { + id: "actor-1", + companyId: "company-1", + name: "Bridge agent", + role: "engineer", + status: "active", + budgetId: "budget-1", + capabilityGrants: [], + }, + ], }); await authority.start(); await authority.openFixtureRun({ - identity: { runId: identity.runId, sessionId: identity.normalizedSessionId, companyId: "company-1", issueId: "task-1", agentId: "actor-1" }, - backendKind: "mock", sourceInstanceId: "durable-tool-bridge", capabilities: [], + identity: { + runId: identity.runId, + sessionId: identity.normalizedSessionId, + companyId: "company-1", + issueId: "task-1", + agentId: "actor-1", + }, + backendKind: "mock", + sourceInstanceId: "durable-tool-bridge", + capabilities: [], }); const dispatcher = new CapabilitySemanticDispatcher(authority); const core = new DurableRecoveryMockCore({ - stateDirectory: resolve(root, "mock-core"), identity, fault: "none", - onSemanticToolInput: (call) => dispatcher.dispatch({ runId: identity.runId, ...call }), + stateDirectory: resolve(root, "mock-core"), + identity, + fault: "none", + onSemanticToolInput: (call) => + dispatcher.dispatch({ runId: identity.runId, ...call }), }); const authorized = dispatcher.listTools(identity.runId); - const operations: Array<{ operationId: string; version: number; description: string; inputSchema: unknown; responseSchema: unknown }> = authorized.map((tool) => { + const operations: Array<{ + operationId: string; + version: number; + description: string; + inputSchema: unknown; + responseSchema: unknown; + }> = authorized.map((tool) => { const descriptor = capabilitySemanticToolDescriptor(tool.name); if (!descriptor) throw new Error(`missing descriptor for ${tool.name}`); - return { operationId: tool.name, version: 1, description: tool.description, inputSchema: tool.inputSchema, responseSchema: descriptor.outputSchema }; + return { + operationId: tool.name, + version: 1, + description: tool.description, + inputSchema: tool.inputSchema, + responseSchema: descriptor.outputSchema, + }; }); const catalogDigest = `sha256:${createHash("sha256").update(canonicalJson(operations)).digest("hex")}`; - const providerCommand = options.realCodex ? (process.env.PAPERCLIP_CODEX_COMMAND ?? "codex") : fakeCodexBinary; + const providerCommand = options.realCodex + ? (process.env.PAPERCLIP_CODEX_COMMAND ?? "codex") + : fakeCodexBinary; const providerArgs = options.realCodex ? [ - "-c", 'default_permissions="paperclip-runner-workspace-only"', - "-c", `permissions.paperclip-runner-workspace-only.filesystem={":root"="none",":minimal"="read",":tmpdir"="write",${JSON.stringify(tmpdir())}="write"}`, - "-c", "permissions.paperclip-runner-workspace-only.network.enabled=false", - "-c", "shell_environment_policy.inherit=\"none\"", + "-c", + 'default_permissions="paperclip-runner-workspace-only"', + "-c", + `permissions.paperclip-runner-workspace-only.filesystem={":root"="none",":minimal"="read",":tmpdir"="write",${JSON.stringify(tmpdir())}="write"}`, + "-c", + "permissions.paperclip-runner-workspace-only.network.enabled=false", + "-c", + 'shell_environment_policy.inherit="none"', "app-server", ] : []; @@ -1644,17 +1889,27 @@ export async function runDurableToolBridgeConformance( operations, }, provider: { - command: providerCommand, args: providerArgs, cwd: tmpdir(), model: options.model ?? null, - instructions: "You are in a Paperclip protocol conformance run. Call get_task_context exactly once, use its result, then reply briefly. Do not use shell or filesystem tools.", + command: providerCommand, + args: providerArgs, + cwd: tmpdir(), + model: options.model ?? null, + instructions: + "You are in a Paperclip protocol conformance run. Call get_task_context exactly once, use its result, then reply briefly. Do not use shell or filesystem tools.", }, }); core.queueCommand("session.open", { reuse: "same_session" }); - core.queueCommand("turn.start", { text: "Call get_task_context now and report the task title." }); + core.queueCommand("turn.start", { + text: "Call get_task_context now and report the task title.", + }); await core.start(); const ticket = core.issueBootstrapTicket(); const handle = spawnRunner({ - connectUrl: core.connectUrl, stateDirectory: runnerStateDirectory, - identity, ticket, maxOutboxBytes: 64 * 1024, p0ReserveBytes: 32 * 1024, + connectUrl: core.connectUrl, + stateDirectory: runnerStateDirectory, + identity, + ticket, + maxOutboxBytes: 64 * 1024, + p0ReserveBytes: 32 * 1024, maxRuntimeMs: options.realCodex ? 180_000 : 10_000, }); let callId = ""; @@ -1662,32 +1917,52 @@ export async function runDurableToolBridgeConformance( try { const deadline = Date.now() + (options.realCodex ? 150_000 : 10_000); while (Date.now() < deadline) { - const input = core.store.state.committedEvents.find((event) => - event.eventType === "semantic_tool.input" || event.eventType === "mcp_app.tool_input" - ); - const semantic = ((input?.envelope.payload as Record | undefined)?.payload as Record | undefined)?.semantic_tool as Record | undefined; - if (semantic && typeof semantic.callId === "string" && typeof semantic.operationId === "string") { + const input = core.store.state.committedEvents.find( + (event) => + event.eventType === "semantic_tool.input" || + event.eventType === "mcp_app.tool_input", + ); + const semantic = ( + (input?.envelope.payload as Record | undefined) + ?.payload as Record | undefined + )?.semantic_tool as Record | undefined; + if ( + semantic && + typeof semantic.callId === "string" && + typeof semantic.operationId === "string" + ) { callId = semantic.callId; operationId = semantic.operationId; break; } await new Promise((resolveWait) => setTimeout(resolveWait, 10)); } - if (!callId) throw new Error("runnerd did not forward the provider tool call"); + if (!callId) + throw new Error("runnerd did not forward the provider tool call"); const resultDeadline = Date.now() + 10_000; while (Date.now() < resultDeadline) { - const resultCommand = core.store.state.commands.find((command) => command.type === "semantic_tool.result"); - const turnCompleted = core.store.state.committedEvents.some((event) => event.eventType === "turn.completed"); + const resultCommand = core.store.state.commands.find( + (command) => command.type === "semantic_tool.result", + ); + const turnCompleted = core.store.state.committedEvents.some( + (event) => event.eventType === "turn.completed", + ); if (resultCommand?.status === "completed" && turnCompleted) break; await new Promise((resolveWait) => setTimeout(resolveWait, 10)); } core.queueCommand("runner.shutdown", {}, undefined, true); const exited = await waitForProcess(handle); - const resultCommand = core.store.state.commands.find((command) => command.type === "semantic_tool.result"); + const resultCommand = core.store.state.commands.find( + (command) => command.type === "semantic_tool.result", + ); return { - operationId, callId, toolInputCommitted: true, + operationId, + callId, + toolInputCommitted: true, toolResultCompleted: resultCommand?.status === "completed", - providerTurnCompleted: core.store.state.committedEvents.some((event) => event.eventType === "turn.completed"), + providerTurnCompleted: core.store.state.committedEvents.some( + (event) => event.eventType === "turn.completed", + ), runnerExitCode: exited.code, }; } finally { @@ -1709,12 +1984,22 @@ export interface DurableEvalSessionInput { explicitClaims: string[]; turnTimeoutMs: number; toolExposure?: "eager" | "lazy"; - provider?: "codex" | "opencode" | "aws_agentcore" | "acpx"; + /** Defaults on for Codex; set false only for an eval that needs the baseline prompt. */ + includeCollaborationModeInstructions?: boolean; + provider?: "codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx"; acpxAgent?: QualifiedAcpxAgent; acpxSidecarPath?: string; opencodeVersion?: string; opencodeCommand?: string; opencodeProxyPath?: string; + managedProfile?: { + profileId: string; + anthropicAgentId: string; + agentVersion: string; + environmentId: string; + betaVersion: "managed-agents-2026-04-01"; + maxSessionListCostUsd: number; + }; agentCoreProfile?: { profileId: string; region: string; @@ -1738,6 +2023,7 @@ export interface DurableEvalSessionInput { export type DurableEvalInfrastructureFailureClass = | "provider_turn_timeout" + | "provider_budget_reached" | "runner_shutdown_timeout" | "runner_exit_failure"; @@ -1769,7 +2055,8 @@ function evalLifecycleDiagnostics( commandId: command.commandId, type: command.type, status: command.status, - deliveryCount: core.store.state.commandDeliveryCounts[command.commandId] ?? 0, + deliveryCount: + core.store.state.commandDeliveryCounts[command.commandId] ?? 0, })); const events = core.store.state.committedEvents.slice(-20).map((event) => ({ sourceSeq: event.sourceSeq, @@ -1780,12 +2067,53 @@ function evalLifecycleDiagnostics( let runnerDiagnostics: unknown[] = []; if (runnerStateDirectory) { try { - const state = JSON.parse(readFileSync(resolve(runnerStateDirectory, "runner-state.json"), "utf8")) as { diagnostics?: unknown[] }; - runnerDiagnostics = Array.isArray(state.diagnostics) ? state.diagnostics.slice(-20) : []; + const state = JSON.parse( + readFileSync( + resolve(runnerStateDirectory, "runner-state.json"), + "utf8", + ), + ) as { diagnostics?: unknown[] }; + runnerDiagnostics = Array.isArray(state.diagnostics) + ? state.diagnostics.slice(-20) + : []; } catch { // A runner that failed before its first durable save has no state diagnostics. } } + let providerDescriptor: Record | null = null; + let usageRunDelta: Record | null = null; + for ( + let index = core.store.state.committedEvents.length - 1; + index >= 0; + index -= 1 + ) { + const event = core.store.state.committedEvents[index]?.envelope.payload as + Record | undefined; + const payload = event?.payload as Record | undefined; + if (usageRunDelta === null && event?.eventType === "usage.reported") { + const candidate = payload?.runDelta; + if ( + typeof candidate === "object" && + candidate !== null && + !Array.isArray(candidate) + ) { + usageRunDelta = structuredClone(candidate as Record); + } + } + if (providerDescriptor === null && event?.eventType === "harness.ready") { + const candidate = payload?.providerDescriptor; + if ( + typeof candidate === "object" && + candidate !== null && + !Array.isArray(candidate) + ) { + providerDescriptor = structuredClone( + candidate as Record, + ); + } + } + if (providerDescriptor !== null && usageRunDelta !== null) break; + } return { runnerPid: handle.child.pid ?? null, runnerExitCode: handle.child.exitCode, @@ -1795,12 +2123,22 @@ function evalLifecycleDiagnostics( commands, recentEvents: events, runnerDiagnostics, + providerDescriptor, + providerSessionId: + typeof providerDescriptor?.providerSessionId === "string" + ? providerDescriptor.providerSessionId + : null, + usageRunDelta, }; } -function projectedSemanticResult(operationId: unknown, result: unknown): unknown { +function projectedSemanticResult( + operationId: unknown, + result: unknown, +): unknown { if (operationId !== "invoke_discovered_capability") return result; - const contentItems = (result as { contentItems?: unknown } | null)?.contentItems; + const contentItems = (result as { contentItems?: unknown } | null) + ?.contentItems; if (!Array.isArray(contentItems)) return result; for (const item of contentItems) { const text = (item as { text?: unknown } | null)?.text; @@ -1814,14 +2152,63 @@ function projectedSemanticResult(operationId: unknown, result: unknown): unknown return result; } -function sanitizedAwsEvalEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv { +function claudeManagedEvalProviderConfiguration( + model: string, + profile: NonNullable, +): Record { + return { + kind: "claude_managed", + model, + profileId: profile.profileId, + anthropicAgentId: profile.anthropicAgentId, + agentVersion: profile.agentVersion, + environmentId: profile.environmentId, + betaVersion: profile.betaVersion, + maxSessionListCostUsd: profile.maxSessionListCostUsd, + }; +} + +function managedProviderBudgetReached( + events: readonly { envelope: Record }[], +): boolean { + return events.some((committed) => { + const event = committed.envelope.payload as + Record | undefined; + if (event?.eventType !== "session.updated") return false; + const payload = event.payload as Record | undefined; + return payload?.status === "budget_reached"; + }); +} + +function sanitizedAwsEvalEnvironment( + source: NodeJS.ProcessEnv, +): NodeJS.ProcessEnv { const result: NodeJS.ProcessEnv = {}; for (const key of [ - "PATH", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "SSL_CERT_FILE", "SSL_CERT_DIR", - "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy", - "RUST_BACKTRACE", "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_CONFIG_FILE", - "AWS_SHARED_CREDENTIALS_FILE", "AWS_WEB_IDENTITY_TOKEN_FILE", "AWS_ROLE_ARN", - "AWS_ROLE_SESSION_NAME", "AWS_CONTAINER_CREDENTIALS_FULL_URI", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "PATH", + "LANG", + "LC_ALL", + "LC_CTYPE", + "TZ", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "no_proxy", + "RUST_BACKTRACE", + "AWS_PROFILE", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_CONFIG_FILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_ROLE_ARN", + "AWS_ROLE_SESSION_NAME", + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", ]) { if (typeof source[key] === "string") result[key] = source[key]; } @@ -1829,7 +2216,9 @@ function sanitizedAwsEvalEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessE } /** Production-topology eval: the only Paperclip process in the sandbox is runnerd. */ -export async function runDurableEvalSession(input: DurableEvalSessionInput): Promise> { +export async function runDurableEvalSession( + input: DurableEvalSessionInput, +): Promise> { const identity = { runnerInstanceId: `runner_${createHash("sha256").update(input.attemptId).digest("hex").slice(0, 16)}`, environmentLeaseId: `lease_${createHash("sha256").update(`${input.attemptId}:lease`).digest("hex").slice(0, 16)}`, @@ -1844,14 +2233,19 @@ export async function runDurableEvalSession(input: DurableEvalSessionInput): Pro const authority = new CapabilityMockControlPlaneAdapter(input.seed); const initialState = JSON.stringify(authority.snapshot()); await authority.start(); - const companyId = authority.snapshot().actors.find((actor) => actor.id === input.actorId)?.companyId ?? "company-1"; + const companyId = + authority.snapshot().actors.find((actor) => actor.id === input.actorId) + ?.companyId ?? "company-1"; await authority.openFixtureRun({ identity: { - runId: identity.runId, sessionId: identity.normalizedSessionId, + runId: identity.runId, + sessionId: identity.normalizedSessionId, companyId, - issueId: input.taskId, agentId: input.actorId, + issueId: input.taskId, + agentId: input.actorId, }, - backendKind: "mock", sourceInstanceId: identity.runnerInstanceId, + backendKind: "mock", + sourceInstanceId: identity.runnerInstanceId, capabilities: input.capabilities, }); const dispatcher = new CapabilitySemanticDispatcher(authority, { @@ -1861,38 +2255,86 @@ export async function runDurableEvalSession(input: DurableEvalSessionInput): Pro const authorized = dispatcher.listTools(identity.runId); const loadedOperations = new Set(); const discoveryEvidence: Array> = []; - const operations: Array<{ operationId: string; version: number; description: string; inputSchema: unknown; responseSchema: unknown }> = authorized.map((tool) => { + const operations: Array<{ + operationId: string; + version: number; + description: string; + inputSchema: unknown; + responseSchema: unknown; + }> = authorized.map((tool) => { const descriptor = capabilitySemanticToolDescriptor(tool.name); if (!descriptor) throw new Error(`missing descriptor for ${tool.name}`); - return { operationId: tool.name, version: 1, description: tool.description, inputSchema: tool.inputSchema, responseSchema: descriptor.outputSchema }; + return { + operationId: tool.name, + version: 1, + description: tool.description, + inputSchema: tool.inputSchema, + responseSchema: descriptor.outputSchema, + }; }); - const exposedOperations = input.toolExposure === "lazy" - ? operations.filter((operation) => authorized.find((tool) => tool.name === operation.operationId)?.annotations.exposure === "always") - : operations; + const exposedOperations = + input.toolExposure === "lazy" + ? operations.filter( + (operation) => + authorized.find((tool) => tool.name === operation.operationId) + ?.annotations.exposure === "always", + ) + : operations; if (input.toolExposure === "lazy") { - exposedOperations.push(...CAPABILITY_DISCOVERY_GATEWAY_DEFINITIONS.map((gateway) => ({ - operationId: gateway.name, version: 1, description: gateway.description, - inputSchema: gateway.inputSchema, responseSchema: gateway.outputSchema, - }))); + exposedOperations.push( + ...CAPABILITY_DISCOVERY_GATEWAY_DEFINITIONS.map((gateway) => ({ + operationId: gateway.name, + version: 1, + description: gateway.description, + inputSchema: gateway.inputSchema, + responseSchema: gateway.outputSchema, + })), + ); } const core = new DurableRecoveryMockCore({ - stateDirectory: resolve(root, "mock-core"), identity, fault: "none", + stateDirectory: resolve(root, "mock-core"), + identity, + fault: "none", onSemanticToolInput: async (call) => { if (call.operationId === "discover_capabilities") { const args = (call.input ?? {}) as Record; - const found = dispatcher.discoverTools(identity.runId, String(args.query ?? ""), { - ...(typeof args.namespace === "string" ? { namespace: args.namespace } : {}), - ...(typeof args.limit === "number" ? { limit: args.limit } : {}), + const found = dispatcher.discoverTools( + identity.runId, + String(args.query ?? ""), + { + ...(typeof args.namespace === "string" + ? { namespace: args.namespace } + : {}), + ...(typeof args.limit === "number" ? { limit: args.limit } : {}), + }, + ); + for (const operation of found.operations) + loadedOperations.add(operation.name); + discoveryEvidence.push({ + action: "loaded", + query: found.query, + namespace: found.namespace ?? "", + operationIds: found.operations.map((operation) => operation.name), }); - for (const operation of found.operations) loadedOperations.add(operation.name); - discoveryEvidence.push({ action: "loaded", query: found.query, namespace: found.namespace ?? "", operationIds: found.operations.map((operation) => operation.name) }); return found; } if (call.operationId === "invoke_discovered_capability") { const args = (call.input ?? {}) as Record; const operationId = String(args.operationId ?? ""); - if (!loadedOperations.has(operationId)) return { ok: false, denial: { code: "operation_not_loaded", message: "Operation was not loaded by capability discovery." } }; - const invoked = await dispatcher.dispatch({ runId: identity.runId, callId: call.callId, operationId, input: args.input }); + if (!loadedOperations.has(operationId)) + return { + ok: false, + denial: { + code: "operation_not_loaded", + message: "Operation was not loaded by capability discovery.", + }, + }; + const invoked = await dispatcher.dispatch({ + runId: identity.runId, + callId: call.callId, + operationId, + input: args.input, + }); return { success: invoked.ok, contentItems: [{ type: "inputText", text: JSON.stringify(invoked) }], @@ -1900,99 +2342,212 @@ export async function runDurableEvalSession(input: DurableEvalSessionInput): Pro } return dispatcher.dispatch({ runId: identity.runId, ...call }); }, - connectionLeaseTtlMs: input.turnTimeoutMs + 60_000, + // The connection lease is an ownership/security boundary, not a provider-turn + // deadline. ACP turns are handled synchronously by runnerd today, so a slow + // provider cannot receive runner.shutdown until the turn returns. Keep the + // eval lease aligned with the production live transport and let the separate + // turn timeout classify/cancel provider work without invalidating otherwise + // durable evidence during shutdown. + connectionLeaseTtlMs: durableEvalConnectionLeaseTtlMs(input.turnTimeoutMs), }); const providerArgs = [ - "-c", 'default_permissions="paperclip-runner-workspace-only"', - "-c", `permissions.paperclip-runner-workspace-only.filesystem={":root"="none",":minimal"="read",":tmpdir"="write",${JSON.stringify(tmpdir())}="write"}`, - "-c", "permissions.paperclip-runner-workspace-only.network.enabled=false", - "-c", "shell_environment_policy.inherit=\"none\"", + "-c", + 'default_permissions="paperclip-runner-workspace-only"', + "-c", + `permissions.paperclip-runner-workspace-only.filesystem={":root"="none",":minimal"="read",":tmpdir"="write",${JSON.stringify(tmpdir())}="write"}`, + "-c", + "permissions.paperclip-runner-workspace-only.network.enabled=false", + "-c", + 'shell_environment_policy.inherit="none"', "app-server", ]; const providerKind = input.provider ?? "codex"; - if (providerKind === "aws_agentcore" && input.agentCoreProfile === undefined) { - throw new Error("AWS AgentCore eval requires an immutable qualified profile snapshot"); + if (providerKind === "claude_managed" && input.managedProfile === undefined) { + throw new Error( + "Claude Managed eval requires an immutable managed profile snapshot", + ); } - const opencodeProxyPath = input.opencodeProxyPath - ?? fileURLToPath(new URL("../cli/opencode-app-server-proxy.js", import.meta.url)); - const acpxSidecarPath = input.acpxSidecarPath - ?? fileURLToPath(new URL("../cli/acpx-runtime-sidecar.js", import.meta.url)); - const acpxProfile = providerKind === "acpx" - ? resolveQualifiedAcpxProfile(input.acpxAgent ?? "pi", input.model) - : null; + if ( + providerKind === "aws_agentcore" && + input.agentCoreProfile === undefined + ) { + throw new Error( + "AWS AgentCore eval requires an immutable qualified profile snapshot", + ); + } + const opencodeProxyPath = + input.opencodeProxyPath ?? + fileURLToPath( + new URL("../cli/opencode-app-server-proxy.js", import.meta.url), + ); + const acpxSidecarPath = + input.acpxSidecarPath ?? + fileURLToPath(new URL("../cli/acpx-runtime-sidecar.js", import.meta.url)); + const acpxProfile = + providerKind === "acpx" + ? resolveQualifiedAcpxProfile(input.acpxAgent ?? "pi", input.model) + : null; core.queueCommand("run.prepare", { authorizedTools: { - schema: "paperclip.runner.authorized-tools.v1", schemaVersion: 1, + schema: "paperclip.runner.authorized-tools.v1", + schemaVersion: 1, catalogDigest: `sha256:${createHash("sha256").update(canonicalJson(operations)).digest("hex")}`, operations: exposedOperations, }, - provider: providerKind === "aws_agentcore" ? { - kind: "aws_agentcore", - model: input.model, - ...input.agentCoreProfile, - } : providerKind === "acpx" ? { - kind: "acpx", - agent: acpxProfile!.agent, - model: input.model, - acpxVersion: acpxProfile!.acpxVersion, - agentServerPackage: acpxProfile!.agentServerPackage, - agentServerVersion: acpxProfile!.agentServerVersion, - agentRuntimePackage: acpxProfile!.agentRuntimePackage, - agentRuntimeVersion: acpxProfile!.agentRuntimeVersion, - commandDigest: acpxProfile!.commandDigest, - sidecarCommand: process.execPath, - sidecarArgs: [acpxSidecarPath], - runtimeDirectory: resolve(root, "acpx"), - normalizedSessionId: identity.normalizedSessionId, - runId: identity.runId, - cwd: tmpdir(), - instructions: "You are a Paperclip agent. Use only the supplied Paperclip tools for company state. Follow the user request, then reply concisely. Do not use shell or filesystem tools.", - permissionPolicy: "interactive", - } : { - kind: providerKind, - command: providerKind === "opencode" ? process.execPath : process.env.PAPERCLIP_CODEX_COMMAND ?? "codex", - args: providerKind === "opencode" ? [opencodeProxyPath] : providerArgs, - cwd: tmpdir(), model: input.model, - instructions: "You are a Paperclip agent. Use only the supplied Paperclip tools for company state. Follow the user request, then reply concisely. Do not use shell or filesystem tools.", - }, + provider: + providerKind === "claude_managed" + ? claudeManagedEvalProviderConfiguration( + input.model, + input.managedProfile!, + ) + : providerKind === "aws_agentcore" + ? { + kind: "aws_agentcore", + model: input.model, + ...input.agentCoreProfile, + } + : providerKind === "acpx" + ? { + kind: "acpx", + agent: acpxProfile!.agent, + model: input.model, + acpxVersion: acpxProfile!.acpxVersion, + agentServerPackage: acpxProfile!.agentServerPackage, + agentServerVersion: acpxProfile!.agentServerVersion, + agentRuntimePackage: acpxProfile!.agentRuntimePackage, + agentRuntimeVersion: acpxProfile!.agentRuntimeVersion, + commandDigest: acpxProfile!.commandDigest, + sidecarCommand: process.execPath, + sidecarArgs: [acpxSidecarPath], + runtimeDirectory: resolve(root, "acpx"), + normalizedSessionId: identity.normalizedSessionId, + runId: identity.runId, + cwd: tmpdir(), + instructions: + "You are a Paperclip agent. Use only the supplied Paperclip tools for company state. Follow the user request, then reply concisely. Do not use shell or filesystem tools.", + permissionPolicy: "interactive", + } + : { + kind: providerKind, + command: + providerKind === "opencode" + ? process.execPath + : (process.env.PAPERCLIP_CODEX_COMMAND ?? "codex"), + args: + providerKind === "opencode" + ? [opencodeProxyPath] + : providerArgs, + cwd: tmpdir(), + model: input.model, + instructions: + "You are a Paperclip agent. Use only the supplied Paperclip tools for company state. Follow the user request, then reply concisely. Do not use shell or filesystem tools.", + collaborationMode: "default", + includeCollaborationModeInstructions: + providerKind === "codex" && + (input.includeCollaborationModeInstructions ?? true), + }, }); core.queueCommand("session.open", { reuse: "same_session" }); core.queueCommand("turn.start", { text: input.prompt }); await core.start(); const handle = spawnRunner({ - connectUrl: core.connectUrl, stateDirectory: runnerStateDirectory, identity, - ticket: core.issueBootstrapTicket(), maxOutboxBytes: 256 * 1024, - p0ReserveBytes: 64 * 1024, maxRuntimeMs: input.turnTimeoutMs + 30_000, + connectUrl: core.connectUrl, + stateDirectory: runnerStateDirectory, + identity, + ticket: core.issueBootstrapTicket(), + maxOutboxBytes: 256 * 1024, + p0ReserveBytes: 64 * 1024, + maxRuntimeMs: input.turnTimeoutMs + 30_000, runnerBinaryPath: input.runnerBinaryPath, - environment: providerKind === "opencode" - ? { - ...process.env, - PAPERCLIP_OPENCODE_COMMAND: input.opencodeCommand ?? "opencode", - PAPERCLIP_OPENCODE_RUNTIME_DIR: resolve(root, "opencode"), - PAPERCLIP_RUNNER_INSTANCE_ID: identity.runnerInstanceId, - PAPERCLIP_RUN_ID: identity.runId, - PAPERCLIP_NORMALIZED_SESSION_ID: identity.normalizedSessionId, - } - : providerKind === "aws_agentcore" - ? sanitizedAwsEvalEnvironment(process.env) - : providerKind === "acpx" - ? createSanitizedAcpxEnvironment(process.env, acpxProfile!.agent) - : process.env, + environment: + providerKind === "opencode" + ? { + ...process.env, + PAPERCLIP_OPENCODE_COMMAND: input.opencodeCommand ?? "opencode", + PAPERCLIP_OPENCODE_RUNTIME_DIR: resolve(root, "opencode"), + PAPERCLIP_RUNNER_INSTANCE_ID: identity.runnerInstanceId, + PAPERCLIP_RUN_ID: identity.runId, + PAPERCLIP_NORMALIZED_SESSION_ID: identity.normalizedSessionId, + } + : providerKind === "claude_managed" + ? createSanitizedClaudeManagedEnvironment(process.env) + : providerKind === "aws_agentcore" + ? sanitizedAwsEvalEnvironment(process.env) + : providerKind === "acpx" + ? createSanitizedAcpxEnvironment(process.env, acpxProfile!.agent) + : process.env, }); try { + const resolvedEvalRuntimeRequests = new Set(); + let providerTerminalFailure: { + failureClass: "provider_turn_timeout" | "provider_budget_reached"; + message: string; + retryable: boolean; + } | null = null; const deadline = Date.now() + input.turnTimeoutMs; while (Date.now() < deadline) { - if (core.store.state.committedEvents.some((event) => event.eventType === "turn.completed")) break; + if (providerKind === "acpx") { + for (const committed of core.store.state.committedEvents) { + const event = committed.envelope.payload as + Record | undefined; + if (event?.eventType !== "runtime_request.created") continue; + const payload = event.payload as Record | undefined; + const request = payload?.request as + Record | undefined; + const requestId = + typeof request?.requestId === "string" ? request.requestId : ""; + const requestTurnId = + typeof request?.turnId === "string" + ? request.turnId + : identity.turnId; + if (!requestId || resolvedEvalRuntimeRequests.has(requestId)) + continue; + resolvedEvalRuntimeRequests.add(requestId); + core.queueCommand( + "request.resolve", + { + requestId, + turnId: requestTurnId, + resolution: { action: "decline" }, + }, + `command_eval_permission_${createHash("sha256").update(requestId).digest("hex").slice(0, 16)}`, + true, + ); + } + } + const budgetReached = + providerKind === "claude_managed" && + managedProviderBudgetReached(core.store.state.committedEvents); + if (budgetReached) { + providerTerminalFailure = { + failureClass: "provider_budget_reached", + message: `Claude Managed session reached its $${input.managedProfile!.maxSessionListCostUsd.toFixed(2)} provider list-cost ceiling`, + retryable: false, + }; + break; + } + if ( + core.store.state.committedEvents.some( + (event) => event.eventType === "turn.completed", + ) + ) + break; await new Promise((resolveWait) => setTimeout(resolveWait, 20)); } - if (!core.store.state.committedEvents.some((event) => event.eventType === "turn.completed")) { - throw new DurableEvalInfrastructureError( - "provider_turn_timeout", - `durable provider turn timed out after ${input.turnTimeoutMs}ms`, - true, - evalLifecycleDiagnostics(core, handle, runnerStateDirectory), - ); + if ( + providerTerminalFailure === null && + !core.store.state.committedEvents.some( + (event) => event.eventType === "turn.completed", + ) + ) { + providerTerminalFailure = { + failureClass: "provider_turn_timeout", + message: `durable provider turn timed out after ${input.turnTimeoutMs}ms`, + retryable: true, + }; } + // Managed sessions are retained by normal runner shutdown. Never issue + // session.destroy from the eval lane, including timeout and budget stops. core.queueCommand("runner.shutdown", {}, undefined, true); let exited: RunnerProcessResult; try { @@ -2010,64 +2565,196 @@ export async function runDurableEvalSession(input: DurableEvalSessionInput): Pro "runner_exit_failure", `runnerd exited ${String(exited.code)}: ${exited.stderr}`, false, - { ...evalLifecycleDiagnostics(core, handle, runnerStateDirectory), stderr: exited.stderr.slice(-16_384) }, + { + ...evalLifecycleDiagnostics(core, handle, runnerStateDirectory), + stderr: exited.stderr.slice(-16_384), + }, ); } - const records = core.store.state.committedEvents.map((committed) => committed.envelope.payload as Record); - const evidence: Array> = [{ - id: "exposure-1", kind: "tool_exposure", at: new Date().toISOString(), turnId: null, - data: { operationIds: exposedOperations.map((operation) => operation.operationId) }, - }]; + if (providerTerminalFailure !== null) { + throw new DurableEvalInfrastructureError( + providerTerminalFailure.failureClass, + providerTerminalFailure.message, + providerTerminalFailure.retryable, + evalLifecycleDiagnostics(core, handle, runnerStateDirectory), + ); + } + const records = core.store.state.committedEvents.map( + (committed) => committed.envelope.payload as Record, + ); + const evidence: Array> = [ + { + id: "exposure-1", + kind: "tool_exposure", + at: new Date().toISOString(), + turnId: null, + data: { + operationIds: exposedOperations.map( + (operation) => operation.operationId, + ), + }, + }, + ]; for (const data of discoveryEvidence) { - evidence.push({ id: `discovery-${evidence.length + 1}`, kind: "tool_discovery", at: new Date().toISOString(), turnId: identity.turnId, data }); + evidence.push({ + id: `discovery-${evidence.length + 1}`, + kind: "tool_discovery", + at: new Date().toISOString(), + turnId: identity.turnId, + data, + }); } let evidenceIndex = 1; let assistantText = ""; let semanticResult: Record | null = null; - let usage: Record = {}; + let usage: Record = { + providerCalls: 1, + providerRequests: providerKind === "claude_managed" ? 0 : 1, + inputTokens: 0, + outputTokens: 0, + cachedInputTokens: 0, + reasoningTokens: 0, + providerCostNanodollars: 0, + }; let providerSessionId: string | null = null; + let providerPid: number | null = null; + let sidecarPid: number | null = null; + let agentPid: number | null = null; + let providerDriver: string | null = null; + let providerVersion: string | null = null; + let agentServerVersion: string | null = null; + let agentRuntimeVersion: string | null = null; + let acpProtocolVersion: number | null = null; + let acpxRecordId: string | null = null; + let providerExecutionKind: string | null = null; + let providerService: string | null = null; const projectedCalls = new Map(); for (const record of records) { const payload = record.payload as Record | undefined; if (record.eventType === "harness.ready") { - const descriptor = payload?.providerDescriptor as Record | undefined; - if (typeof descriptor?.providerSessionId === "string") providerSessionId = descriptor.providerSessionId; + const descriptor = payload?.providerDescriptor as + Record | undefined; + if (typeof descriptor?.providerSessionId === "string") + providerSessionId = descriptor.providerSessionId; + if (typeof descriptor?.processId === "number") { + providerPid = descriptor.processId; + if (descriptor.driver === "acpx_runtime") + sidecarPid = descriptor.processId; + } + if (typeof descriptor?.driver === "string") + providerDriver = descriptor.driver; + if (typeof descriptor?.providerVersion === "string") + providerVersion = descriptor.providerVersion; + if (typeof descriptor?.agentServerVersion === "string") + agentServerVersion = descriptor.agentServerVersion; + if (typeof descriptor?.agentRuntimeVersion === "string") + agentRuntimeVersion = descriptor.agentRuntimeVersion; + if (typeof descriptor?.acpProtocolVersion === "number") + acpProtocolVersion = descriptor.acpProtocolVersion; + if (typeof descriptor?.agentProcessId === "number") + agentPid = descriptor.agentProcessId; + if (typeof descriptor?.acpxRecordId === "string") + acpxRecordId = descriptor.acpxRecordId; + if (typeof descriptor?.executionKind === "string") + providerExecutionKind = descriptor.executionKind; + if (typeof descriptor?.service === "string") + providerService = descriptor.service; } - if (record.eventType === "run.result.proposed" && payload && semanticResult === null) { + if (record.eventType === "harness.diagnostic") { + if ( + payload?.providerMethod === "acpx/process" && + payload.role === "acp_agent" && + typeof payload.pid === "number" + ) { + agentPid = payload.pid; + } + } + if ( + record.eventType === "run.result.proposed" && + payload && + semanticResult === null + ) { semanticResult = structuredClone(payload); } - const semantic = payload?.semantic_tool as Record | undefined; + const semantic = payload?.semantic_tool as + Record | undefined; if (semantic?.phase === "input" || semantic?.phase === "result") { - const wrapperInput = semantic.input as Record | undefined; - const projectedOperationId = semantic.operationId === "invoke_discovered_capability" - ? (typeof wrapperInput?.operationId === "string" ? wrapperInput.operationId : projectedCalls.get(String(semantic.callId)) ?? semantic.operationId) - : semantic.operationId; - const projectedInput = semantic.operationId === "invoke_discovered_capability" ? wrapperInput?.input : semantic.input; - if (semantic.phase === "input") projectedCalls.set(String(semantic.callId), String(projectedOperationId)); + const wrapperInput = semantic.input as + Record | undefined; + const projectedOperationId = + semantic.operationId === "invoke_discovered_capability" + ? typeof wrapperInput?.operationId === "string" + ? wrapperInput.operationId + : (projectedCalls.get(String(semantic.callId)) ?? + semantic.operationId) + : semantic.operationId; + const projectedInput = + semantic.operationId === "invoke_discovered_capability" + ? wrapperInput?.input + : semantic.input; + if (semantic.phase === "input") + projectedCalls.set( + String(semantic.callId), + String(projectedOperationId), + ); evidenceIndex += 1; evidence.push({ - id: `evidence-${evidenceIndex}`, kind: semantic.phase === "input" ? "tool_call" : "tool_result", - at: new Date().toISOString(), turnId: identity.turnId, - data: semantic.phase === "input" - ? { callId: semantic.callId, operationId: projectedOperationId, input: projectedInput } - : { callId: semantic.callId, operationId: projectedOperationId, result: projectedSemanticResult(semantic.operationId, semantic.result) }, + id: `evidence-${evidenceIndex}`, + kind: semantic.phase === "input" ? "tool_call" : "tool_result", + at: new Date().toISOString(), + turnId: identity.turnId, + data: + semantic.phase === "input" + ? { + callId: semantic.callId, + operationId: projectedOperationId, + input: projectedInput, + } + : { + callId: semantic.callId, + operationId: projectedOperationId, + result: projectedSemanticResult( + semantic.operationId, + semantic.result, + ), + }, }); } if (record.eventType === "provider.event") { const method = payload?.method; const params = payload?.params as Record | undefined; - if (typeof params?.delta === "string" && String(method).includes("agentMessage")) assistantText += params.delta; + if ( + typeof params?.delta === "string" && + String(method).includes("agentMessage") + ) + assistantText += params.delta; const item = params?.item as Record | undefined; // Codex reports the completed assistant item through `item/completed`; the // item type, rather than the notification method, identifies its role. - if (!assistantText && item?.type === "agentMessage" && typeof item.text === "string") assistantText = item.text; - if (!assistantText && String(method).includes("agentMessage") && typeof params?.text === "string") assistantText = params.text; - if (method === "thread/tokenUsage/updated" && typeof params?.tokenUsage === "object" && params.tokenUsage) { + if ( + !assistantText && + item?.type === "agentMessage" && + typeof item.text === "string" + ) + assistantText = item.text; + if ( + !assistantText && + String(method).includes("agentMessage") && + typeof params?.text === "string" + ) + assistantText = params.text; + if ( + method === "thread/tokenUsage/updated" && + typeof params?.tokenUsage === "object" && + params.tokenUsage + ) { const tokenUsage = params.tokenUsage as Record; - const total = typeof tokenUsage.total === "object" && tokenUsage.total !== null - ? tokenUsage.total as Record - : tokenUsage as Record; + const total = + typeof tokenUsage.total === "object" && tokenUsage.total !== null + ? (tokenUsage.total as Record) + : (tokenUsage as Record); usage = { + ...usage, inputTokens: total.inputTokens ?? 0, outputTokens: total.outputTokens ?? 0, cachedInputTokens: total.cachedInputTokens ?? 0, @@ -2081,16 +2768,27 @@ export async function runDurableEvalSession(input: DurableEvalSessionInput): Pro } if (record.eventType === "item.completed") { const item = payload?.item as Record | undefined; - if (item?.type === "agentMessage" && typeof item.text === "string") assistantText = item.text; + if (item?.type === "agentMessage" && typeof item.text === "string") + assistantText = item.text; } if (record.eventType === "usage.reported") { - const runDelta = payload?.runDelta as Record | undefined; + const runDelta = payload?.runDelta as + Record | undefined; if (runDelta) { usage = { + ...usage, + providerRequests: + runDelta.requests ?? + runDelta.providerRequests ?? + usage.providerRequests, inputTokens: runDelta.inputTokens ?? 0, outputTokens: runDelta.outputTokens ?? 0, cachedInputTokens: runDelta.cacheReadTokens ?? 0, reasoningTokens: runDelta.reasoningTokens ?? 0, + providerCostNanodollars: + typeof runDelta.providerCostUsd === "number" + ? Math.round(runDelta.providerCostUsd * 1_000_000_000) + : usage.providerCostNanodollars, }; } } @@ -2102,69 +2800,216 @@ export async function runDurableEvalSession(input: DurableEvalSessionInput): Pro const item = candidate as Record; return item.type === "agentMessage" && typeof item.text === "string"; }) as Record | undefined; - if (typeof finalMessage?.text === "string") assistantText = finalMessage.text; + if (typeof finalMessage?.text === "string") + assistantText = finalMessage.text; } } for (const command of core.store.state.commands) { - if (command.type !== "semantic_tool.result" || command.status !== "completed") continue; + if ( + command.type !== "semantic_tool.result" || + command.status !== "completed" + ) + continue; const payload = command.payload as Record; const wrapperInput = payload.input as Record | undefined; - const projectedOperationId = payload.operationId === "invoke_discovered_capability" - ? (typeof wrapperInput?.operationId === "string" ? wrapperInput.operationId : projectedCalls.get(String(payload.callId)) ?? payload.operationId) - : payload.operationId; - if (evidence.some((entry) => entry.kind === "tool_result" && (entry.data as Record).callId === payload.callId)) continue; + const projectedOperationId = + payload.operationId === "invoke_discovered_capability" + ? typeof wrapperInput?.operationId === "string" + ? wrapperInput.operationId + : (projectedCalls.get(String(payload.callId)) ?? + payload.operationId) + : payload.operationId; + if ( + evidence.some( + (entry) => + entry.kind === "tool_result" && + (entry.data as Record).callId === payload.callId, + ) + ) + continue; evidenceIndex += 1; evidence.push({ - id: `evidence-${evidenceIndex}`, kind: "tool_result", at: new Date().toISOString(), turnId: identity.turnId, - data: { callId: payload.callId, operationId: projectedOperationId, result: projectedSemanticResult(payload.operationId, payload.result) }, + id: `evidence-${evidenceIndex}`, + kind: "tool_result", + at: new Date().toISOString(), + turnId: identity.turnId, + data: { + callId: payload.callId, + operationId: projectedOperationId, + result: projectedSemanticResult(payload.operationId, payload.result), + }, }); } - if (!assistantText) assistantText = "Provider completed the requested Paperclip operation."; + if (!assistantText) + assistantText = "Provider completed the requested Paperclip operation."; const finalState = JSON.stringify(authority.snapshot()); const now = new Date().toISOString(); return { turn: { turnId: identity.turnId, status: "completed", assistantText }, snapshot: { - schema: "paperclip.capability.live-session.v1", revision: authority.snapshot().revision, - sessionId: identity.normalizedSessionId, providerThreadId: "withheld", providerSessionId, + schema: "paperclip.capability.live-session.v1", + revision: authority.snapshot().revision, + sessionId: identity.normalizedSessionId, + providerThreadId: "withheld", + providerSessionId, providerModel: { id: input.model, - provider: providerKind === "opencode" - ? input.model.split("/", 1)[0] - : providerKind === "aws_agentcore" - ? "amazon-bedrock" - : providerKind === "acpx" - ? acpxProfile!.agent === "pi" ? "openrouter" : acpxProfile!.agent === "claude" ? "anthropic" : "openai" - : "openai", + provider: + providerKind === "opencode" + ? input.model.split("/", 1)[0] + : providerKind === "claude_managed" + ? "anthropic" + : providerKind === "aws_agentcore" + ? "amazon-bedrock" + : providerKind === "acpx" + ? acpxProfile!.agent === "pi" + ? "openrouter" + : acpxProfile!.agent === "claude" + ? "anthropic" + : "openai" + : "openai", }, - modelHistory: [{ - requestedModel: input.model, - effectiveModel: input.model, - at: now, - source: "provider_verified", - }], - status: "idle", activeTurnId: null, + modelHistory: [ + { + requestedModel: input.model, + effectiveModel: input.model, + at: now, + source: "provider_verified", + }, + ], + status: "idle", + activeTurnId: null, semanticResult, - createdAt: now, updatedAt: now, - authority: { active: true, runId: identity.runId, companyId, actorId: input.actorId, taskId: input.taskId, sessionId: identity.normalizedSessionId, scenarioId: input.attemptId, capabilities: input.capabilities, explicitClaims: input.explicitClaims }, - config: { workingDirectory: providerKind === "aws_agentcore" ? null : tmpdir(), requestedModel: input.model, provider: providerKind, driver: providerKind === "opencode" ? "opencode_server" : providerKind === "aws_agentcore" ? "aws_agentcore_harness_api" : providerKind === "acpx" ? "acpx_runtime" : "codex_app_server", providerVersion: providerKind === "opencode" ? input.opencodeVersion ?? "1.18.17" : providerKind === "aws_agentcore" ? input.agentCoreProfile?.harnessVersion ?? null : providerKind === "acpx" ? acpxProfile!.acpxVersion : null, ...(acpxProfile === null ? {} : { acpxAgent: acpxProfile.agent, acpxProfile }), scenario: { id: input.attemptId, claims: input.capabilities }, capabilities: input.capabilities, explicitClaims: input.explicitClaims, seedState: initialState, turnTimeoutMs: input.turnTimeoutMs }, + createdAt: now, + updatedAt: now, + authority: { + active: true, + runId: identity.runId, + companyId, + actorId: input.actorId, + taskId: input.taskId, + sessionId: identity.normalizedSessionId, + scenarioId: input.attemptId, + capabilities: input.capabilities, + explicitClaims: input.explicitClaims, + }, + config: { + workingDirectory: + providerKind === "claude_managed" || + providerKind === "aws_agentcore" + ? null + : tmpdir(), + requestedModel: input.model, + provider: providerKind, + driver: + providerKind === "opencode" + ? "opencode_server" + : providerKind === "claude_managed" + ? "claude_managed_agents_api" + : providerKind === "aws_agentcore" + ? "aws_agentcore_harness_api" + : providerKind === "acpx" + ? "acpx_runtime" + : "codex_app_server", + providerVersion: + providerKind === "opencode" + ? (input.opencodeVersion ?? "1.18.17") + : providerKind === "claude_managed" + ? (input.managedProfile?.agentVersion ?? null) + : providerKind === "aws_agentcore" + ? (input.agentCoreProfile?.harnessVersion ?? null) + : providerKind === "acpx" + ? acpxProfile!.acpxVersion + : null, + ...(providerKind === "claude_managed" + ? { managedProfile: input.managedProfile } + : {}), + ...(acpxProfile === null + ? {} + : { acpxAgent: acpxProfile.agent, acpxProfile }), + scenario: { id: input.attemptId, claims: input.capabilities }, + capabilities: input.capabilities, + explicitClaims: input.explicitClaims, + seedState: initialState, + turnTimeoutMs: input.turnTimeoutMs, + }, mockState: finalState, transcript: [ - { id: "transcript-user", role: "user", text: input.prompt, turnId: identity.turnId, at: now }, - { id: "transcript-assistant", role: "assistant", text: assistantText, turnId: identity.turnId, at: now }, + { + id: "transcript-user", + role: "user", + text: input.prompt, + turnId: identity.turnId, + at: now, + }, + { + id: "transcript-assistant", + role: "assistant", + text: assistantText, + turnId: identity.turnId, + at: now, + }, + ], + evidence, + authorizationRecords: dispatcher.authorizationRecords(), + process: { + runnerPid: handle.child.pid ?? null, + runnerProcessGroupId: handle.child.pid ?? null, + providerPid, + codexPid: providerKind === "codex" ? providerPid : null, + sidecarPid, + agentPid, + providerDriver, + providerVersion, + acpxAgent: acpxProfile?.agent ?? null, + agentServerVersion, + agentRuntimeVersion, + acpProtocolVersion, + acpxRecordId, + providerExecutionKind, + providerService, + runnerExited: true, + runnerExitCode: exited.code, + runnerSignal: exited.signal, + childEnvironmentKeys: [], + diagnostics: [], + }, + networkEvidence: { + realPaperclipRequests: 0, + childPaperclipEnvironmentKeys: [], + }, + terminalTurns: [ + { + turnId: identity.turnId, + status: "completed", + reconciledByAttemptId: input.attemptId, + observedAt: now, + }, + ], + usageLedger: [ + { + receiptId: `usage-${input.attemptId}`, + attemptId: input.attemptId, + providerResponseId: providerSessionId, + turnId: identity.turnId, + providerCalls: usage.providerCalls ?? 1, + providerRequests: usage.providerRequests ?? 0, + inputTokens: usage.inputTokens ?? 0, + outputTokens: usage.outputTokens ?? 0, + cachedInputTokens: usage.cachedInputTokens ?? 0, + reasoningTokens: usage.reasoningTokens ?? 0, + costNanodollars: usage.providerCostNanodollars ?? 0, + observedAt: now, + }, ], - evidence, authorizationRecords: dispatcher.authorizationRecords(), - process: { runnerPid: handle.child.pid ?? null, codexPid: null, runnerExited: true, runnerExitCode: exited.code, runnerSignal: exited.signal, childEnvironmentKeys: [], diagnostics: [] }, - networkEvidence: { realPaperclipRequests: 0, childPaperclipEnvironmentKeys: [] }, - terminalTurns: [{ turnId: identity.turnId, status: "completed", reconciledByAttemptId: input.attemptId, observedAt: now }], - usageLedger: [{ receiptId: `usage-${input.attemptId}`, attemptId: input.attemptId, providerResponseId: null, turnId: identity.turnId, providerCalls: 1, providerRequests: 1, inputTokens: usage.inputTokens ?? 0, outputTokens: usage.outputTokens ?? 0, cachedInputTokens: usage.cachedInputTokens ?? 0, reasoningTokens: usage.reasoningTokens ?? 0, costNanodollars: 0, observedAt: now }], }, }; } finally { handle.child.kill("SIGKILL"); await core.stop(); await authority.stop(); - if (process.env.PAPERCLIP_KEEP_EVAL_STATE !== "1") rmSync(root, { recursive: true, force: true }); + if (process.env.PAPERCLIP_KEEP_EVAL_STATE !== "1") + rmSync(root, { recursive: true, force: true }); } } @@ -2179,7 +3024,9 @@ export async function runDurableRecoveryRecovery( tmpdir(); const root = options.stateDirectory ?? - mkdtempSync(resolve(scratchRoot, `paperclip-runner-durable-recovery-${fault}-`)); + mkdtempSync( + resolve(scratchRoot, `paperclip-runner-durable-recovery-${fault}-`), + ); const runnerStateDirectory = resolve(root, "runner"); const coreStateDirectory = resolve(root, "mock-core"); mkdirSync(runnerStateDirectory, { recursive: true, mode: 0o700 }); @@ -2220,7 +3067,9 @@ export async function runDurableRecoveryRecovery( } else if (fault === "lease-expiry") { const expired = await waitForProcess(handle); if (expired.code === 0) { - throw new Error("Lease-expiry injection did not produce a recoverable restart boundary."); + throw new Error( + "Lease-expiry injection did not produce a recoverable restart boundary.", + ); } runnerRestarts += 1; handle = launch(); @@ -2256,10 +3105,10 @@ export async function runDurableRecoveryRecovery( [runnerStatePath, core.store.path], [...tickets, "must-not-persist"], ); - const bootstrapTicketPersisted = persistedCapabilityShape( - runnerStatePath, - ["bootstrapTicket", "bootstrap_ticket"], - ); + const bootstrapTicketPersisted = persistedCapabilityShape(runnerStatePath, [ + "bootstrapTicket", + "bootstrap_ticket", + ]); const connectionLeaseTokenPersisted = persistedCapabilityShape( runnerStatePath, ["connectionLeaseToken", "connection_lease_token"], @@ -2267,7 +3116,9 @@ export async function runDurableRecoveryRecovery( const committedEvents = coreState.committedEvents.toSorted( (left, right) => left.sourceSeq - right.sourceSeq, ); - const p0Committed = committedEvents.filter((event) => event.priority === 0).length; + const p0Committed = committedEvents.filter( + (event) => event.priority === 0, + ).length; const completedCommands = coreState.commands.filter( (command) => command.status === "completed", ); @@ -2282,10 +3133,9 @@ export async function runDurableRecoveryRecovery( : 0), 0, ); - const duplicateDeliveries = Object.values(coreState.commandDeliveryCounts).reduce( - (sum, count) => sum + Math.max(0, count - 1), - 0, - ); + const duplicateDeliveries = Object.values( + coreState.commandDeliveryCounts, + ).reduce((sum, count) => sum + Math.max(0, count - 1), 0); const outcome: DurableRecoveryDiagnostics["recovery"]["outcome"] = fault === "drain" ? "drained" @@ -2318,7 +3168,10 @@ export async function runDurableRecoveryRecovery( maxBytes: maxOutboxBytes, backpressure: runnerState.backpressure, p0Committed, - p0Lost: runnerState.ackedSourceSeq === coreState.ackedSourceSeq ? 0 : p0Committed, + p0Lost: + runnerState.ackedSourceSeq === coreState.ackedSourceSeq + ? 0 + : p0Committed, }, commands: { issued: coreState.commands.length, @@ -2398,4 +3251,7 @@ export const durableRecoveryInternals = { canonicalJson, durableRecoveryIdentity, projectedSemanticResult, + claudeManagedEvalProviderConfiguration, + managedProviderBudgetReached, + durableEvalConnectionLeaseTtlMs, }; diff --git a/packages/paperclip-runner/src/devtools/index.ts b/packages/paperclip-runner/src/devtools/index.ts index cef19c1defa..8aec5b4b123 100644 --- a/packages/paperclip-runner/src/devtools/index.ts +++ b/packages/paperclip-runner/src/devtools/index.ts @@ -1,4 +1,7 @@ -import type { CapabilityJsonValue, CapabilityFixtureState } from "../mock-core/capability-control-plane-types.js"; +import type { + CapabilityJsonValue, + CapabilityFixtureState, +} from "../mock-core/capability-control-plane-types.js"; import type { CapabilityLiveSessionSnapshot } from "../live/live-session.js"; export interface CapabilityDevtoolsRevision { @@ -18,6 +21,13 @@ export interface CapabilityDevtoolsProtocolRecord { detail: CapabilityJsonValue; } +export interface CapabilityDevtoolsProviderTrace { + status: "capturing" | "complete" | "incomplete" | "truncated"; + expiresAt: string; + frames: CapabilityJsonValue[]; + interpretations: CapabilityJsonValue[]; +} + export interface CapabilityDevtoolsSnapshot { schema: "paperclip.capability.devtools.v1"; currentRevision: number; @@ -25,25 +35,36 @@ export interface CapabilityDevtoolsSnapshot { protocol: CapabilityDevtoolsProtocolRecord[]; runtime: CapabilityJsonValue; authority: CapabilityJsonValue; + /** Present only for an explicitly traced run; never populated by ordinary snapshots. */ + providerTrace?: CapabilityDevtoolsProviderTrace | null; } -const SECRET = /(bearer\s+[a-z0-9._-]+|(?:^|[^a-z0-9])(?:sk|pcp)_[a-z0-9._-]{8,}|api[_-]?key\s*[:=]|PAPERCLIP_API_KEY|OPENAI_API_KEY)/i; -const SECRET_KEY = /(^|[_-])(authorization|api[_-]?key|token|secret|password)($|[_-])|^(apiKey|accessToken|refreshToken)$/i; +const SECRET = + /(bearer\s+[a-z0-9._-]+|(?:^|[^a-z0-9])(?:sk|pcp)_[a-z0-9._-]{8,}|api[_-]?key\s*[:=]|PAPERCLIP_API_KEY|OPENAI_API_KEY)/i; +const SECRET_KEY = + /(^|[_-])(authorization|api[_-]?key|token|secret|password)($|[_-])|^(apiKey|accessToken|refreshToken)$/i; function safe(value: unknown, key = ""): CapabilityJsonValue { if (SECRET_KEY.test(key)) return "[redacted]"; - if (value === null || typeof value === "boolean" || typeof value === "number") return value; - if (typeof value === "string") return SECRET.test(value) ? "[redacted]" : value; + if (value === null || typeof value === "boolean" || typeof value === "number") + return value; + if (typeof value === "string") + return SECRET.test(value) ? "[redacted]" : value; if (Array.isArray(value)) return value.map((entry) => safe(entry)); if (typeof value !== "object") return String(value); - return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, safe(entry, entryKey)])); + return Object.fromEntries( + Object.entries(value).map(([entryKey, entry]) => [ + entryKey, + safe(entry, entryKey), + ]), + ); } function parseState(serialized: string): CapabilityFixtureState { return JSON.parse(serialized) as CapabilityFixtureState; } -/** Field-by-field browser projection. Provider payloads and checkpoints never cross this boundary. */ +/** Field-by-field browser projection. Raw provider payloads cross only through the opt-in trace channel. */ function publicCompanyState(serialized: string): CapabilityJsonValue { const state = parseState(serialized); return safe({ @@ -61,10 +82,12 @@ function publicCompanyState(serialized: string): CapabilityJsonValue { documents: state.documents, interactions: state.interactions, approvals: state.approvals, - artifacts: state.artifacts.map(({ contentRef: _contentRef, ...artifact }) => ({ - ...artifact, - contentRef: "[withheld]", - })), + artifacts: state.artifacts.map( + ({ contentRef: _contentRef, ...artifact }) => ({ + ...artifact, + contentRef: "[withheld]", + }), + ), workProducts: state.workProducts, blockers: state.blockers, workspaceServices: state.workspaceServices, @@ -90,37 +113,52 @@ function publicCompanyState(serialized: string): CapabilityJsonValue { wakes: state.wakes, audit: state.audit, decisions: state.decisions, - idempotency: state.idempotency.map(({ inputCanonical: _inputCanonical, ...record }) => record), + idempotency: state.idempotency.map( + ({ inputCanonical: _inputCanonical, ...record }) => record, + ), faults: state.faults, }); } function boundary(kind: string): string { - if (kind === "tool_call" || kind === "tool_result" || kind === "tool_discovery") return "semantic tool"; + if ( + kind === "tool_call" || + kind === "tool_result" || + kind === "tool_discovery" + ) + return "semantic tool"; if (kind === "provider_event") return "provider harness"; if (kind === "process") return "runnerd process"; if (kind === "interaction") return "mock control plane"; return "live session"; } -export function projectCapabilityDevtools(snapshot: CapabilityLiveSessionSnapshot): CapabilityDevtoolsSnapshot { - const retained = snapshot.stateHistory ?? [{ - revision: parseState(snapshot.mockState).revision, - at: snapshot.updatedAt, - turnId: null, - operationId: "session.current", - state: snapshot.mockState, - }]; +export function projectCapabilityDevtools( + snapshot: CapabilityLiveSessionSnapshot, +): CapabilityDevtoolsSnapshot { + const retained = snapshot.stateHistory ?? [ + { + revision: parseState(snapshot.mockState).revision, + at: snapshot.updatedAt, + turnId: null, + operationId: "session.current", + state: snapshot.mockState, + }, + ]; const seed = parseState(snapshot.config.seedState); - const history = retained[0]?.revision === seed.revision - ? retained - : [{ - revision: seed.revision, - at: snapshot.createdAt, - turnId: null, - operationId: "fixture.seed", - state: snapshot.config.seedState, - }, ...retained]; + const history = + retained[0]?.revision === seed.revision + ? retained + : [ + { + revision: seed.revision, + at: snapshot.createdAt, + turnId: null, + operationId: "fixture.seed", + state: snapshot.config.seedState, + }, + ...retained, + ]; return { schema: "paperclip.capability.devtools.v1", currentRevision: parseState(snapshot.mockState).revision, @@ -144,15 +182,18 @@ export function projectCapabilityDevtools(snapshot: CapabilityLiveSessionSnapsho activeTurnId: snapshot.activeTurnId, providerSessionId: snapshot.providerSessionId, providerModel: snapshot.providerModel ?? null, - process: snapshot.process === null ? null : { - runnerPid: snapshot.process.runnerPid, - runnerProcessGroupId: snapshot.process.runnerProcessGroupId, - codexPid: snapshot.process.codexPid, - runnerExited: snapshot.process.runnerExited, - runnerExitCode: snapshot.process.runnerExitCode, - runnerSignal: snapshot.process.runnerSignal, - childEnvironmentKeys: snapshot.process.childEnvironmentKeys, - }, + process: + snapshot.process === null + ? null + : { + runnerPid: snapshot.process.runnerPid, + runnerProcessGroupId: snapshot.process.runnerProcessGroupId, + codexPid: snapshot.process.codexPid, + runnerExited: snapshot.process.runnerExited, + runnerExitCode: snapshot.process.runnerExitCode, + runnerSignal: snapshot.process.runnerSignal, + childEnvironmentKeys: snapshot.process.childEnvironmentKeys, + }, network: snapshot.networkEvidence, attempts: snapshot.attempts ?? [], currentAttemptId: snapshot.currentAttemptId ?? null, @@ -172,5 +213,6 @@ export function projectCapabilityDevtools(snapshot: CapabilityLiveSessionSnapsho toolExposure: snapshot.config.toolExposure ?? "eager", workingDirectory: "[withheld]", }), + providerTrace: null, }; } diff --git a/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-driver.ts b/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-driver.ts index 36958512773..719310315ed 100644 --- a/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-driver.ts +++ b/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-driver.ts @@ -40,6 +40,7 @@ import { type AcpxRuntimeFactory, type AcpxRuntimeIdentity, type AcpxRuntimePermissionContext, + type AcpxRuntimeUsage, } from "./acpx-runtime-host.js"; import { ACPX_DRIVER_KIND, @@ -183,6 +184,7 @@ export class AcpxRuntimeDriver implements HarnessDriver { }, runtimeFactory: this.#options.runtimeFactory, onSpawn: this.#options.onSpawn, + onUsage: (usage) => session?.acceptUsage(usage), onDiagnostic: this.#options.onDiagnostic, }); try { @@ -443,6 +445,22 @@ class AcpxHarnessSession implements HarnessSession { } } + acceptUsage(usage: AcpxRuntimeUsage): void { + const turnId = this.#activeTurnId; + if (!turnId) return; + this.#captureUsage({ + cumulative: { + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens, + cachedReadTokens: usage.cachedReadTokens, + cachedWriteTokens: usage.cachedWriteTokens, + thoughtTokens: usage.thoughtTokens, + totalTokens: usage.totalTokens, + }, + cost: usage.cost, + }, turnId, `${turnId}:usage-live:${this.#sourceSequence + 1}`); + } + async read(): Promise> { return { identity: this.#host.identity(), status: await this.#host.status() }; } diff --git a/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-host.test.ts b/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-host.test.ts index 58b3e3af397..d026fd3aa22 100644 --- a/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-host.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-host.test.ts @@ -31,6 +31,7 @@ describe("AcpxRuntimeHost", () => { await mkdir(workspace); let captured: AcpRuntimeOptions | null = null; let ensureInput: Parameters[0] | null = null; + const usages: unknown[] = []; const handle: AcpRuntimeHandle = { sessionKey: "session-key", backend: "acpx", @@ -77,6 +78,7 @@ describe("AcpxRuntimeHost", () => { DATABASE_URL: "database-must-not-pass", }, dynamicToolHandler: async () => ({ ok: true }), + onUsage: (usage) => usages.push(usage), runtimeFactory, }); try { @@ -108,6 +110,14 @@ describe("AcpxRuntimeHost", () => { expect(JSON.stringify(childEnvironment)).not.toContain("database-must-not-pass"); expect(JSON.stringify(ensureInput)).not.toContain("openrouter-canary-secret"); expect(JSON.stringify(ensureInput)).not.toContain("paperclip-must-not-pass"); + options.onAgentStderr?.("[paperclip-pi-usage-v1]{\"inputTokens\":12,\"outputTokens\":3,"); + options.onAgentStderr?.("\"totalTokens\":15,\"cost\":{\"amount\":0.001,\"currency\":\"USD\"}}\n"); + expect(usages).toEqual([{ + inputTokens: 12, + outputTokens: 3, + totalTokens: 15, + cost: { amount: 0.001, currency: "USD" }, + }]); expect((await stat(host.runtimeRoot())).mode & 0o777).toBe(0o700); const settingsPath = join(host.runtimeRoot(), "pi-home", "settings.json"); expect(JSON.parse(await readFile(settingsPath, "utf8"))).toMatchObject({ @@ -132,6 +142,218 @@ describe("AcpxRuntimeHost", () => { })).rejects.toThrow("requires exact model"); }); + it("verifies Claude's exact canonical model through its ACP selector", async () => { + const root = await mkdtemp(join(tmpdir(), "paperclip-acpx-claude-model-")); + const workspace = join(root, "workspace"); + await mkdir(workspace); + const setCalls: Array<{ key: string; value: unknown }> = []; + let capturedOptions: AcpRuntimeOptions | null = null; + let externalPermissionRequests = 0; + const host = await AcpxRuntimeHost.open({ + runtimeDirectory: root, + normalizedSessionId: "claude-model-session", + workingDirectory: workspace, + agent: "claude", + model: "claude-sonnet-5", + environment: { PATH: process.env.PATH }, + dynamicToolHandler: async () => ({}), + onPermissionRequest: async () => { + externalPermissionRequests += 1; + return { outcome: "reject_once" }; + }, + runtimeFactory: (options) => { + capturedOptions = options; + return { + ensureSession: async () => ({ + sessionKey: "claude-session-key", + backend: "acpx", + runtimeSessionName: "claude-runtime", + acpxRecordId: "claude-record", + backendSessionId: "claude-backend", + agentSessionId: "claude-agent", + }), + startTurn: (input) => completedTurn(input.requestId), + runTurn: async function* () {}, + getStatus: async () => ({ + acpxRecordId: "claude-record", + backendSessionId: "claude-backend", + agentSessionId: "claude-agent", + models: { currentModelId: "sonnet", availableModelIds: ["default", "sonnet", "opus"] }, + }), + setConfigOption: async (input) => { setCalls.push({ key: input.key, value: input.value }); }, + cancel: async () => {}, + close: async () => {}, + }; + }, + }); + try { + expect(setCalls).toEqual([{ key: "model", value: "claude-sonnet-5" }]); + expect(host.identity()).toMatchObject({ + requestedModel: "claude-sonnet-5", + effectiveModel: "claude-sonnet-5", + }); + expect((await host.status()).models).toMatchObject({ + currentModelId: "claude-sonnet-5", + availableModelIds: ["default", "claude-sonnet-5", "opus"], + }); + const permissionHandler = (capturedOptions as AcpRuntimeOptions).onPermissionRequest!; + await expect(permissionHandler({ + sessionId: "claude-agent", + inferredKind: "other", + raw: { + sessionId: "claude-agent", + toolCall: { + toolCallId: "tool-paperclip", + title: "mcp__paperclip__get_task_context", + kind: "other", + }, + options: [], + }, + }, { signal: new AbortController().signal })).resolves.toEqual({ outcome: "allow_once" }); + expect(externalPermissionRequests).toBe(0); + await expect(permissionHandler({ + sessionId: "claude-agent", + inferredKind: "execute", + raw: { + sessionId: "claude-agent", + toolCall: { toolCallId: "tool-bash", title: "Bash", kind: "execute" }, + options: [], + }, + }, { signal: new AbortController().signal })).resolves.toEqual({ outcome: "reject_once" }); + expect(externalPermissionRequests).toBe(1); + } finally { + await host.close({ reason: "test complete" }); + } + }); + + it("rejects an unverified Claude selector even after an exact model set", async () => { + await expect(AcpxRuntimeHost.open({ + runtimeDirectory: await mkdtemp(join(tmpdir(), "paperclip-acpx-claude-drift-")), + normalizedSessionId: "claude-model-drift", + workingDirectory: process.cwd(), + agent: "claude", + model: "claude-sonnet-5", + dynamicToolHandler: async () => ({}), + runtimeFactory: () => ({ + ensureSession: async () => ({ + sessionKey: "claude-drift-key", + backend: "acpx", + runtimeSessionName: "claude-drift-runtime", + acpxRecordId: "claude-drift-record", + backendSessionId: "claude-drift-backend", + agentSessionId: "claude-drift-agent", + }), + startTurn: (input) => completedTurn(input.requestId), + runTurn: async function* () {}, + getStatus: async () => ({ + models: { currentModelId: "default", availableModelIds: ["default", "sonnet"] }, + }), + setConfigOption: async () => {}, + cancel: async () => {}, + close: async () => {}, + }), + })).rejects.toThrow("expected ACP selector sonnet"); + }); + + it("stages managed Codex auth only in the isolated home and removes it on close", async () => { + const root = await mkdtemp(join(tmpdir(), "paperclip-acpx-codex-auth-")); + const workspace = join(root, "workspace"); + const sourceAuth = join(root, "managed-auth.json"); + await mkdir(workspace); + await writeFile(sourceAuth, JSON.stringify({ tokens: { access_token: "managed-codex-canary" } }), { mode: 0o600 }); + let isolatedAuthPath = ""; + const host = await AcpxRuntimeHost.open({ + runtimeDirectory: root, + normalizedSessionId: "codex-session", + workingDirectory: workspace, + agent: "codex", + model: "gpt-5.6-sol", + environment: { PATH: process.env.PATH }, + managedCredentialSources: { codexAuthPath: sourceAuth }, + dynamicToolHandler: async () => ({}), + runtimeFactory: (options) => ({ + ensureSession: async (input) => { + const environment = (options as AcpRuntimeOptions & { spawnEnvironment?: () => Record }).spawnEnvironment?.() ?? {}; + expect(JSON.stringify(input)).not.toContain("managed-codex-canary"); + expect(environment.CODEX_HOME).toContain("codex-home"); + isolatedAuthPath = join(environment.CODEX_HOME!, "auth.json"); + expect(await readFile(isolatedAuthPath, "utf8")).toContain("managed-codex-canary"); + return { + sessionKey: "codex-session-key", + backend: "acpx", + runtimeSessionName: "codex-runtime", + acpxRecordId: "codex-record", + backendSessionId: "codex-backend", + agentSessionId: "codex-agent", + }; + }, + startTurn: (input) => completedTurn(input.requestId), + runTurn: async function* () {}, + getStatus: async () => ({ + acpxRecordId: "codex-record", + backendSessionId: "codex-backend", + agentSessionId: "codex-agent", + models: { currentModelId: "gpt-5.6-sol", availableModelIds: ["gpt-5.6-sol"] }, + }), + cancel: async () => {}, + close: async () => {}, + }), + }); + expect((await stat(isolatedAuthPath)).mode & 0o777).toBe(0o600); + await host.close({ reason: "test complete" }); + await expect(readFile(isolatedAuthPath)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(sourceAuth, "utf8")).toContain("managed-codex-canary"); + }); + + it("retains the record but still closes Pi when backend session deletion is unsupported", async () => { + const root = await mkdtemp(join(tmpdir(), "paperclip-acpx-close-")); + const workspace = join(root, "workspace"); + await mkdir(workspace); + const closeAttempts: boolean[] = []; + const host = await AcpxRuntimeHost.open({ + runtimeDirectory: root, + normalizedSessionId: "pi-close-session", + workingDirectory: workspace, + agent: "pi", + model: "openrouter/deepseek/deepseek-v4-flash-0731", + environment: { PATH: process.env.PATH }, + dynamicToolHandler: async () => ({}), + runtimeFactory: () => ({ + ensureSession: async () => ({ + sessionKey: "pi-close-key", + backend: "acpx", + runtimeSessionName: "pi-close-runtime", + acpxRecordId: "pi-close-record", + backendSessionId: "pi-close-backend", + agentSessionId: "pi-close-agent", + }), + startTurn: (input) => completedTurn(input.requestId), + runTurn: async function* () {}, + getStatus: async () => ({ + acpxRecordId: "pi-close-record", + backendSessionId: "pi-close-backend", + agentSessionId: "pi-close-agent", + models: { + currentModelId: "openrouter/deepseek/deepseek-v4-flash-0731", + availableModelIds: ["openrouter/deepseek/deepseek-v4-flash-0731"], + }, + }), + cancel: async () => {}, + close: async (input) => { + closeAttempts.push(input.discardPersistentState === true); + if (input.discardPersistentState) { + const error = new Error("Pi does not implement session/close") as Error & { code: string }; + error.code = "ACP_BACKEND_UNSUPPORTED_CONTROL"; + throw error; + } + }, + }), + }); + + await expect(host.close({ reason: "environment probe", discardPersistentState: true })).resolves.toBeUndefined(); + expect(closeAttempts).toEqual([true, false]); + }); + it("rejects recovery identity drift before constructing or spawning the runtime", async () => { const root = await mkdtemp(join(tmpdir(), "paperclip-acpx-recovery-")); const workspace = join(root, "workspace"); @@ -144,7 +366,7 @@ describe("AcpxRuntimeHost", () => { agentSessionId: "agent-1", requestedModel: "openrouter/deepseek/deepseek-v4-flash-0731", effectiveModel: "openrouter/deepseek/deepseek-v4-flash-0731", - profileDigest: "sha256:5d06b5cfe819b1acd75e8d9dea4766e76a40a64b82549eda08c3835f18bdac24", + profileDigest: "sha256:e806321f458baaf23aa5580324d8f90a59082066105eda69de35b1ef0c8418eb", })); let runtimeConstructed = false; await expect(AcpxRuntimeHost.open({ @@ -159,7 +381,7 @@ describe("AcpxRuntimeHost", () => { acpxRecordId: "record-stale", backendSessionId: "backend-1", agentSessionId: "agent-1", - profileDigest: "sha256:5d06b5cfe819b1acd75e8d9dea4766e76a40a64b82549eda08c3835f18bdac24", + profileDigest: "sha256:e806321f458baaf23aa5580324d8f90a59082066105eda69de35b1ef0c8418eb", workspaceDigest: `sha256:${createHash("sha256").update(resolve(workspace)).digest("hex")}`, requestedModel: "openrouter/deepseek/deepseek-v4-flash-0731", effectiveModel: "openrouter/deepseek/deepseek-v4-flash-0731", diff --git a/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-host.ts index 57105bd392b..6da02ea0fb6 100644 --- a/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/acpx-runtime-host.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; -import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; +import { chmod, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; +import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -42,6 +43,16 @@ export interface AcpxRuntimeProcessIdentity { startedAt: string; } +export interface AcpxRuntimeUsage { + inputTokens?: number; + outputTokens?: number; + cachedReadTokens?: number; + cachedWriteTokens?: number; + thoughtTokens?: number; + totalTokens?: number; + cost?: { amount: number; currency: "USD" }; +} + export interface AcpxRuntimePermissionContext { request: AcpPermissionRequest; signal: AbortSignal; @@ -83,7 +94,10 @@ export interface AcpxRuntimeHostOptions { context: AcpxRuntimePermissionContext, ) => Promise; onSpawn?: (meta: AcpxRuntimeProcessIdentity) => Promise; + onUsage?: (usage: AcpxRuntimeUsage) => void; onDiagnostic?: (message: string) => void; + /** Host-only source paths; neither paths nor contents enter ACPX records. */ + managedCredentialSources?: { codexAuthPath?: string }; } export interface AcpxRuntimeIdentity { @@ -122,6 +136,8 @@ export class AcpxRuntimeHost { readonly #bridge: RunnerToolBridge; readonly #identity: AcpxRuntimeIdentity; readonly #root: string; + readonly #ephemeralCredentialFiles: string[]; + readonly #agentProcessIdentity: AcpxRuntimeProcessIdentity | null; #activeTurn: AcpRuntimeTurn | null = null; #closed = false; @@ -131,12 +147,16 @@ export class AcpxRuntimeHost { bridge: RunnerToolBridge; identity: AcpxRuntimeIdentity; root: string; + ephemeralCredentialFiles: string[]; + agentProcessIdentity: AcpxRuntimeProcessIdentity | null; }) { this.#runtime = input.runtime; this.#handle = input.handle; this.#bridge = input.bridge; this.#identity = input.identity; this.#root = input.root; + this.#ephemeralCredentialFiles = input.ephemeralCredentialFiles; + this.#agentProcessIdentity = input.agentProcessIdentity; } static async open(options: AcpxRuntimeHostOptions): Promise { @@ -164,6 +184,9 @@ export class AcpxRuntimeHost { enableInstallTelemetry: false, })}\n`, { mode: 0o600 }); } + const ephemeralCredentialFiles = options.agent === "codex" + ? await stageManagedCodexCredential(agentHome, options) + : []; const command = await resolveAndVerifyCommand(profile); const piCommand = options.agent === "pi" ? await resolvePiRuntimeCommand(profile) : null; @@ -210,6 +233,7 @@ export class AcpxRuntimeHost { PAPERCLIP_RUNTIME_ROOT: root, } : {}), ...(options.agent === "claude" ? { CLAUDE_CONFIG_DIR: agentHome } : {}), + ...(options.agent === "codex" ? { CODEX_HOME: agentHome } : {}), PAPERCLIP_ACPX_PROFILE: options.agent, }, options.agent); const safeSessionEnvironment = withoutCredentials(agentEnvironment); @@ -219,6 +243,12 @@ export class AcpxRuntimeHost { url: bridge.url, headers: [{ name: "Authorization", value: `Bearer ${bridge.secret}` }], }]; + const routeAgentStderr = createAgentStderrRouter( + agentEnvironment, + options.onUsage, + options.onDiagnostic, + ); + let agentProcessIdentity: AcpxRuntimeProcessIdentity | null = null; const runtimeOptions: SecureAcpRuntimeOptions = { cwd, spawnCwd: cwd, @@ -235,16 +265,27 @@ export class AcpxRuntimeHost { defaultAction: "escalate", }, elicitationModes: ["form", "url"], - onPermissionRequest: options.onPermissionRequest - ? (request, context) => options.onPermissionRequest!({ request, signal: context.signal }) - : async () => ({ outcome: "reject_once" }), + onPermissionRequest: async (request, context) => { + // Claude Code asks its ACP client to authorize every MCP invocation, + // including calls to Paperclip's own authenticated semantic bridge. + // Those calls must reach PRP, whose attached run catalog and schema + // checks are the authority. Treating this duplicate provider prompt as + // a human approval prevents even read-only semantic tools from running. + // Built-in filesystem/process calls do not have this closed namespace + // and continue through the durable interactive permission flow. + if (options.agent === "claude" && isRunnerOwnedSemanticPermission(request)) { + return { outcome: "allow_once" }; + } + return options.onPermissionRequest + ? options.onPermissionRequest({ request, signal: context.signal }) + : { outcome: "reject_once" }; + }, spawnEnvironment: () => ({ ...agentEnvironment }), - onAgentStderr: (chunk) => options.onDiagnostic?.(redactDiagnostic(chunk, agentEnvironment)), - onAgentSpawn: async (meta) => options.onSpawn?.({ - pid: meta.pid, - processGroupId: null, - startedAt: meta.startedAt, - }), + onAgentStderr: routeAgentStderr, + onAgentSpawn: async (meta) => { + agentProcessIdentity = { pid: meta.pid, processGroupId: null, startedAt: meta.startedAt }; + await options.onSpawn?.(agentProcessIdentity); + }, }; const runtimeFactory = options.runtimeFactory ?? ((input) => createAcpRuntime(input)); const runtime = runtimeFactory(runtimeOptions); @@ -261,7 +302,7 @@ export class AcpxRuntimeHost { env: safeSessionEnvironment, }, }); - const status = await requireVerifiedModel(runtime, handle, options.model); + const status = await requireVerifiedModel(runtime, handle, profile); const backendSessionId = requiredIdentity(status.backendSessionId ?? handle.backendSessionId, "backend session"); const identity: AcpxRuntimeIdentity = { acpxRecordId: requiredIdentity(status.acpxRecordId ?? handle.acpxRecordId, "ACPX record"), @@ -285,12 +326,21 @@ export class AcpxRuntimeHost { effectiveModel: identity.effectiveModel, profileDigest: profile.commandDigest, }, null, 2)}\n`, { mode: 0o600 }); - return new AcpxRuntimeHost({ runtime, handle, bridge, identity, root }); + return new AcpxRuntimeHost({ + runtime, + handle, + bridge, + identity, + root, + ephemeralCredentialFiles, + agentProcessIdentity, + }); } catch (error) { if (handle) { await runtime.close({ handle, reason: "ACPX startup failed", discardPersistentState: false }).catch(() => {}); } await bridge.close().catch(() => {}); + await removeEphemeralCredentialFiles(ephemeralCredentialFiles); throw error; } } @@ -300,9 +350,11 @@ export class AcpxRuntimeHost { } async status(): Promise { - return this.#runtime.getStatus - ? this.#runtime.getStatus({ handle: this.#handle }) - : {}; + if (!this.#runtime.getStatus) return {}; + return normalizeQualifiedModelStatus( + await this.#runtime.getStatus({ handle: this.#handle }), + this.#identity.profile, + ); } startTurn(input: { requestId: string; text: string; signal?: AbortSignal }): AcpRuntimeTurn { @@ -332,17 +384,102 @@ export class AcpxRuntimeHost { if (this.#closed) return; this.#closed = true; await this.#activeTurn?.cancel({ reason: input.reason }).catch(() => {}); - await this.#runtime.close({ - handle: this.#handle, - reason: input.reason, - discardPersistentState: input.discardPersistentState ?? false, - }).finally(() => this.#bridge.close()).catch(async (error) => { + try { + const discardPersistentState = input.discardPersistentState ?? false; + try { + await this.#runtime.close({ + handle: this.#handle, + reason: input.reason, + discardPersistentState, + }); + } catch (error) { + // Some qualified ACP agents (currently Pi) do not implement the + // optional session/close control required for remote destruction. + // Still close the live owner and descendants, but retain the provider + // record rather than leaking a process or pretending it was deleted. + if (!discardPersistentState || runtimeErrorCode(error) !== "ACP_BACKEND_UNSUPPORTED_CONTROL") { + throw error; + } + await this.#runtime.close({ + handle: this.#handle, + reason: `${input.reason} (retaining unsupported backend record)`, + discardPersistentState: false, + }); + } + } finally { await this.#bridge.close().catch(() => {}); - throw error; - }); + await removeEphemeralCredentialFiles(this.#ephemeralCredentialFiles); + } } runtimeRoot(): string { return this.#root; } + + processIdentity(): AcpxRuntimeProcessIdentity | null { + return this.#agentProcessIdentity === null ? null : { ...this.#agentProcessIdentity }; + } +} + +function runtimeErrorCode(error: unknown): string | null { + if (!error || typeof error !== "object" || Array.isArray(error)) return null; + return typeof (error as { code?: unknown }).code === "string" + ? (error as { code: string }).code + : null; +} + +function isRunnerOwnedSemanticPermission(request: AcpPermissionRequest): boolean { + const raw = request.raw as unknown as Record; + const toolCall = raw.toolCall; + if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) return false; + const call = toolCall as Record; + const meta = call._meta; + const claudeCode = meta && typeof meta === "object" && !Array.isArray(meta) + ? (meta as Record).claudeCode + : null; + const metadataToolName = claudeCode && typeof claudeCode === "object" && !Array.isArray(claudeCode) + ? (claudeCode as Record).toolName + : null; + return [call.name, call.title, metadataToolName].some((value) => ( + typeof value === "string" && value.startsWith("mcp__paperclip__") + )); +} + +async function stageManagedCodexCredential( + agentHome: string, + options: AcpxRuntimeHostOptions, +): Promise { + if (options.environment?.OPENAI_API_KEY || options.environment?.CODEX_API_KEY) return []; + const source = options.managedCredentialSources?.codexAuthPath ?? join(homedir(), ".codex", "auth.json"); + let metadata; + try { + metadata = await stat(source); + } catch { + return []; + } + if (!metadata.isFile() || metadata.size <= 0 || metadata.size > 256 * 1024) { + throw new Error("Managed Codex credential source is not a bounded regular file"); + } + const credential = await readFile(source); + try { + const parsed = JSON.parse(credential.toString("utf8")) as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("invalid credential document"); + } + } catch { + credential.fill(0); + throw new Error("Managed Codex credential source is malformed"); + } + const destination = join(agentHome, "auth.json"); + try { + await writeFile(destination, credential, { mode: 0o600 }); + await chmod(destination, 0o600); + } finally { + credential.fill(0); + } + return [destination]; +} + +async function removeEphemeralCredentialFiles(paths: readonly string[]): Promise { + await Promise.all(paths.map((path) => rm(path, { force: true }).catch(() => {}))); } async function verifyExpectedIdentity( @@ -379,20 +516,49 @@ async function verifyExpectedIdentity( async function requireVerifiedModel( runtime: AcpRuntime, handle: AcpRuntimeHandle, - requestedModel: string, + profile: QualifiedAcpxProfile, ): Promise { if (!runtime.getStatus) throw new Error("ACPX agent cannot verify its effective model"); + const requestedModel = profile.qualificationModel; let status = await runtime.getStatus({ handle }); - if (status.models?.currentModelId !== requestedModel && runtime.setConfigOption) { + + // A distinct reported selector is an explicit part of a pinned profile, not + // a fuzzy alias. Re-apply the user's exact canonical model through ACP so a + // stale/default session cannot be mistaken for the qualified model merely + // because it currently reports the same selector. + if (profile.reportedModelId !== requestedModel) { + if (!runtime.setConfigOption) { + throw new Error("ACPX agent cannot verify its canonical model through ACP config options"); + } + await runtime.setConfigOption({ handle, key: "model", value: requestedModel }); + status = await runtime.getStatus({ handle }); + } else if (status.models?.currentModelId !== requestedModel && runtime.setConfigOption) { await runtime.setConfigOption({ handle, key: "model", value: requestedModel }); status = await runtime.getStatus({ handle }); } - if (status.models?.currentModelId !== requestedModel) { + if (status.models?.currentModelId !== profile.reportedModelId) { throw new Error( - `ACPX effective model mismatch: requested ${requestedModel}, received ${status.models?.currentModelId ?? "unverified"}`, + `ACPX effective model mismatch: requested ${requestedModel}, expected ACP selector ${profile.reportedModelId}, received ${status.models?.currentModelId ?? "unverified"}`, ); } - return status; + return normalizeQualifiedModelStatus(status, profile); +} + +function normalizeQualifiedModelStatus( + status: AcpRuntimeStatus, + profile: QualifiedAcpxProfile, +): AcpRuntimeStatus { + if (status.models?.currentModelId !== profile.reportedModelId) return status; + return { + ...status, + models: { + ...status.models, + currentModelId: profile.qualificationModel, + availableModelIds: status.models.availableModelIds.map((modelId) => ( + modelId === profile.reportedModelId ? profile.qualificationModel : modelId + )), + }, + }; } async function resolveAndVerifyCommand(profile: QualifiedAcpxProfile): Promise<{ path: string; digest: string }> { @@ -532,10 +698,13 @@ function withoutCredentials(environment: Record): Record): string { +function redactDiagnostic( + value: string, + environment: Readonly>, +): string { let redacted = value; for (const [key, secret] of Object.entries(environment)) { - if (!/(?:KEY|TOKEN|SECRET|PASSWORD|AUTHORIZATION)$/i.test(key) || secret.length < 4) continue; + if (!secret || !/(?:KEY|TOKEN|SECRET|PASSWORD|AUTHORIZATION)$/i.test(key) || secret.length < 4) continue; redacted = redacted.split(secret).join("[REDACTED]"); } return redacted.replace(/(key|token|secret|password|authorization)\s*[:=]\s*[^\s,}\]]+/gi, "$1=[REDACTED]").slice(-8_192); @@ -569,6 +738,7 @@ function profileSessionKey( cwd, agent: profile.agent, model, + reportedModelId: profile.reportedModelId, commandDigest: profile.commandDigest, driverKind: profile.driverKind, protocolVersion: profile.protocolVersion, @@ -582,6 +752,59 @@ function requiredIdentity(value: string | undefined, label: string): string { return value; } +const PI_USAGE_PREFIX = "[paperclip-pi-usage-v1]"; + +function createAgentStderrRouter( + environment: NodeJS.ProcessEnv, + onUsage?: (usage: AcpxRuntimeUsage) => void, + onDiagnostic?: (message: string) => void, +): (chunk: string) => void { + let pending = ""; + return (chunk) => { + pending = `${pending}${chunk}`.slice(-128 * 1024); + const lines = pending.split(/\r?\n/); + pending = lines.pop() ?? ""; + for (const line of lines) { + if (line.startsWith(PI_USAGE_PREFIX)) { + const usage = parsePiUsage(line.slice(PI_USAGE_PREFIX.length)); + if (usage) onUsage?.(usage); + else onDiagnostic?.("Pi emitted a malformed bounded usage notification."); + continue; + } + if (line) onDiagnostic?.(redactDiagnostic(line, environment)); + } + }; +} + +function parsePiUsage(serialized: string): AcpxRuntimeUsage | null { + if (Buffer.byteLength(serialized) > 8 * 1024) return null; + try { + const value = JSON.parse(serialized) as Record; + const usage: AcpxRuntimeUsage = {}; + for (const key of [ + "inputTokens", + "outputTokens", + "cachedReadTokens", + "cachedWriteTokens", + "thoughtTokens", + "totalTokens", + ] as const) { + const amount = value[key]; + if (typeof amount === "number" && Number.isFinite(amount) && amount >= 0) usage[key] = amount; + } + const cost = value.cost; + if (typeof cost === "object" && cost !== null && !Array.isArray(cost)) { + const amount = (cost as Record).amount; + if (typeof amount === "number" && Number.isFinite(amount) && amount >= 0) { + usage.cost = { amount, currency: "USD" }; + } + } + return Object.keys(usage).length > 0 ? usage : null; + } catch { + return null; + } +} + function paperclipAcpSystemPrompt(): string { return [ "You are running inside Paperclip Runner.", diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index 8d9653c47ad..0ca8df28800 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -16,6 +16,15 @@ export interface QualifiedAcpxProfile { agentRuntimeVersion: string | null; commandDigest: string; qualificationModel: string; + /** + * Model identifier the pinned ACP server reports after accepting the exact + * qualification model. Most agents echo the requested model. Claude's ACP + * server deliberately exposes its stable SDK selector (`sonnet`) while the + * SDK resolves that selector to the canonical wire model + * (`claude-sonnet-5`). Paperclip verifies the exact request was accepted + * before treating this identifier as the qualified effective model. + */ + reportedModelId: string; permissionPolicy: "interactive"; } @@ -35,8 +44,9 @@ export const QUALIFIED_ACPX_PROFILES: Readonly; + /** Internal-only correlation retained outside provider params and canonical PRP payloads. */ + paperclipTrace?: { + sourceEventId: string; + sourceEventType: string; + }; +} + +export interface CodexTraceInterpretation { + sourceEventId: string; + sourceEventType: string; + providerMethod: string; + disposition: "mapped" | "ignored" | "rejected"; + emittedEventIds: string[]; + reason: string; } export interface CodexRpcServerRequest extends CodexRpcNotification { @@ -14,7 +28,10 @@ export type CodexServerRequestHandler = ( ) => Promise>; export interface CodexAppServerTransport { - request(method: string, params: Record): Promise>; + request( + method: string, + params: Record, + ): Promise>; notify(method: string, params?: Record): void; notifications(): AsyncIterable; setServerRequestHandler(handler: CodexServerRequestHandler): void; @@ -25,6 +42,8 @@ export interface CodexAppServerTransport { turnId: string; itemId: string; }): Promise; + /** Records post-rehydration driver mapping without affecting run authority. */ + recordTraceInterpretation?(input: CodexTraceInterpretation): void; } export interface CodexTransportProcessInfo { @@ -58,7 +77,10 @@ class BoundedAsyncQueue implements AsyncIterable { waiter.resolve({ value, done: false }); return true; } - if (this.#values.length >= this.maxValues || this.#queuedBytes + bytes > this.maxBytes) { + if ( + this.#values.length >= this.maxValues || + this.#queuedBytes + bytes > this.maxBytes + ) { return false; } this.#values.push({ value, bytes }); @@ -115,10 +137,15 @@ class BoundedLineDecoder { const end = newline < 0 ? incoming.length : newline; const segment = incoming.subarray(offset, end); if (this.#buffer.length + segment.length > this.maxLineBytes) { - this.#fail(new Error(`codex app-server line exceeded ${this.maxLineBytes} bytes`)); + this.#fail( + new Error( + `codex app-server line exceeded ${this.maxLineBytes} bytes`, + ), + ); return; } - if (segment.length > 0) this.#buffer = Buffer.concat([this.#buffer, segment]); + if (segment.length > 0) + this.#buffer = Buffer.concat([this.#buffer, segment]); if (newline < 0) return; const line = this.#buffer; this.#buffer = Buffer.alloc(0); @@ -131,7 +158,9 @@ class BoundedLineDecoder { end(): void { if (this.#closed) return; if (this.#buffer.length > 0) { - this.#fail(new Error("codex app-server ended with an unterminated JSON-RPC line")); + this.#fail( + new Error("codex app-server ended with an unterminated JSON-RPC line"), + ); } this.#closed = true; } @@ -185,7 +214,9 @@ export function createSanitizedCodexEnvironment( return environment; } -export function sanitizedEnvironmentKeys(source: NodeJS.ProcessEnv = process.env): string[] { +export function sanitizedEnvironmentKeys( + source: NodeJS.ProcessEnv = process.env, +): string[] { return Object.keys(createSanitizedCodexEnvironment(source)).sort(); } @@ -195,10 +226,22 @@ export function redactCodexDiagnostic(message: string): string { .replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [REDACTED]") .replace(/Basic\s+[A-Za-z0-9+/=]+/gi, "Basic [REDACTED]") .replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi, "$1[REDACTED]@") - .replace(/([?&](?:api[_-]?key|token|secret|password)=)[^&#\s]+/gi, "$1[REDACTED]") - .replace(/(["'](?:api[_-]?key|token|secret|password|authorization)["']\s*:\s*["'])[^"']+/gi, "$1[REDACTED]") - .replace(/(api[_-]?key|token|secret|password)\s*[=:]\s*[^\s,;]+/gi, "$1=[REDACTED]") - .replace(/(PAPERCLIP_API_KEY|OPENAI_API_KEY|OPENROUTER_API_KEY)=[^\s]+/g, "$1=[REDACTED]"); + .replace( + /([?&](?:api[_-]?key|token|secret|password)=)[^&#\s]+/gi, + "$1[REDACTED]", + ) + .replace( + /(["'](?:api[_-]?key|token|secret|password|authorization)["']\s*:\s*["'])[^"']+/gi, + "$1[REDACTED]", + ) + .replace( + /(api[_-]?key|token|secret|password)\s*[=:]\s*[^\s,;]+/gi, + "$1=[REDACTED]", + ) + .replace( + /(PAPERCLIP_API_KEY|OPENAI_API_KEY|OPENROUTER_API_KEY)=[^\s]+/g, + "$1=[REDACTED]", + ); } function proxyContainsCredentials(value: string): boolean { @@ -318,7 +361,10 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { constructor(options: ProcessCodexTransportOptions = {}) { this.#onDiagnostic = options.onDiagnostic; - this.#maxLineBytes = positiveLimit(options.maxLineBytes, DEFAULT_MAX_LINE_BYTES); + this.#maxLineBytes = positiveLimit( + options.maxLineBytes, + DEFAULT_MAX_LINE_BYTES, + ); this.#maxPendingRequests = positiveLimit( options.maxPendingRequests, DEFAULT_MAX_PENDING_REQUESTS, @@ -327,7 +373,8 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { options.maxInflightServerRequests, DEFAULT_MAX_INFLIGHT_SERVER_REQUESTS, ); - this.#processGroup = options.processGroup === true && process.platform !== "win32"; + this.#processGroup = + options.processGroup === true && process.platform !== "win32"; this.#startedAt = new Date().toISOString(); this.#closeGraceMs = positiveLimit(options.closeGraceMs, 1_000); this.#onProcess = options.onProcess; @@ -335,17 +382,24 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { this.#resolveExit = resolve; }); this.#notifications = new BoundedAsyncQueue( - positiveLimit(options.maxQueuedNotifications, DEFAULT_MAX_QUEUED_NOTIFICATIONS), + positiveLimit( + options.maxQueuedNotifications, + DEFAULT_MAX_QUEUED_NOTIFICATIONS, + ), positiveLimit( options.maxQueuedNotificationBytes, DEFAULT_MAX_QUEUED_NOTIFICATION_BYTES, ), ); - this.#process = spawn(options.command ?? "codex", options.args ?? ["app-server"], { - env: options.environment ?? createSanitizedCodexEnvironment(), - stdio: "pipe", - detached: this.#processGroup, - }); + this.#process = spawn( + options.command ?? "codex", + options.args ?? ["app-server"], + { + env: options.environment ?? createSanitizedCodexEnvironment(), + stdio: "pipe", + detached: this.#processGroup, + }, + ); this.#onProcess?.(this.processInfo()); this.#stdoutDecoder = new BoundedLineDecoder( this.#maxLineBytes, @@ -355,11 +409,18 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { this.#stderrDecoder = new BoundedLineDecoder( MAX_DIAGNOSTIC_LINE_BYTES, (line) => this.#onDiagnostic?.(redactCodexDiagnostic(line)), - () => this.#onDiagnostic?.("codex app-server diagnostic line exceeded retention limit"), + () => + this.#onDiagnostic?.( + "codex app-server diagnostic line exceeded retention limit", + ), + ); + this.#process.stdout.on("data", (chunk: Buffer) => + this.#stdoutDecoder.write(chunk), ); - this.#process.stdout.on("data", (chunk: Buffer) => this.#stdoutDecoder.write(chunk)); this.#process.stdout.on("end", () => this.#stdoutDecoder.end()); - this.#process.stderr.on("data", (chunk: Buffer) => this.#stderrDecoder.write(chunk)); + this.#process.stderr.on("data", (chunk: Buffer) => + this.#stderrDecoder.write(chunk), + ); this.#process.stderr.on("end", () => this.#stderrDecoder.end()); this.#process.on("error", (error) => this.#fatal(error)); this.#process.on("exit", (code, signal) => { @@ -378,11 +439,17 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { }); } - request(method: string, params: Record): Promise> { - if (this.#closed) return Promise.reject(new Error("codex app-server transport is closed")); + request( + method: string, + params: Record, + ): Promise> { + if (this.#closed) + return Promise.reject(new Error("codex app-server transport is closed")); if (this.#pending.size >= this.#maxPendingRequests) { return Promise.reject( - new Error(`codex app-server pending request limit ${this.#maxPendingRequests} exceeded`), + new Error( + `codex app-server pending request limit ${this.#maxPendingRequests} exceeded`, + ), ); } const id = this.#nextId++; @@ -422,13 +489,17 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { if (firstClose) this.#signal("SIGTERM"); const exited = await Promise.race([ this.#exitPromise.then(() => true), - new Promise((resolve) => setTimeout(() => resolve(false), this.#closeGraceMs)), + new Promise((resolve) => + setTimeout(() => resolve(false), this.#closeGraceMs), + ), ]); if (!exited && !this.#exited) { this.#signal("SIGKILL"); await Promise.race([ this.#exitPromise, - new Promise((resolve) => setTimeout(resolve, this.#closeGraceMs)), + new Promise((resolve) => + setTimeout(resolve, this.#closeGraceMs), + ), ]); } } @@ -437,7 +508,7 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { processInfo(): CodexTransportProcessInfo { return { pid: this.#process.pid ?? null, - processGroupId: this.#processGroup ? this.#process.pid ?? null : null, + processGroupId: this.#processGroup ? (this.#process.pid ?? null) : null, startedAt: this.#startedAt, exited: this.#exited, exitCode: this.#exitCode, @@ -462,7 +533,9 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { if (this.#closed) throw new Error("codex app-server transport is closed"); const serialized = JSON.stringify(message); if (Buffer.byteLength(serialized) > this.#maxLineBytes) { - throw new Error(`outbound codex JSON-RPC line exceeded ${this.#maxLineBytes} bytes`); + throw new Error( + `outbound codex JSON-RPC line exceeded ${this.#maxLineBytes} bytes`, + ); } this.#process.stdin.write(`${serialized}\n`); } @@ -476,56 +549,87 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { return; } if (!isRecord(value)) { - this.#fatal(new Error("codex app-server emitted a non-object JSON-RPC message")); + this.#fatal( + new Error("codex app-server emitted a non-object JSON-RPC message"), + ); return; } const message = value; const hasResult = Object.hasOwn(message, "result"); const hasError = Object.hasOwn(message, "error"); if (typeof message.id === "number" && (hasResult || hasError)) { - if (!Number.isSafeInteger(message.id) || message.id <= 0 || hasResult === hasError) { - this.#fatal(new Error("codex app-server emitted a malformed JSON-RPC response")); + if ( + !Number.isSafeInteger(message.id) || + message.id <= 0 || + hasResult === hasError + ) { + this.#fatal( + new Error("codex app-server emitted a malformed JSON-RPC response"), + ); return; } const pending = this.#pending.get(message.id); if (pending === undefined) { - this.#fatal(new Error("codex app-server responded to an unknown request id")); + this.#fatal( + new Error("codex app-server responded to an unknown request id"), + ); return; } this.#pending.delete(message.id); if (hasError) { - pending.reject(new CodexRpcError( - redactCodexDiagnostic(boundedJson(message.error)), - rpcErrorCode(message.error), - )); + pending.reject( + new CodexRpcError( + redactCodexDiagnostic(boundedJson(message.error)), + rpcErrorCode(message.error), + ), + ); } else if (isRecord(message.result)) { pending.resolve(message.result); } else { - const error = new Error("codex app-server response result was not an object"); + const error = new Error( + "codex app-server response result was not an object", + ); pending.reject(error); this.#fatal(error); } return; } - if (typeof message.method !== "string" || message.method.length === 0 || hasResult || hasError) { - this.#fatal(new Error("codex app-server emitted a malformed JSON-RPC message")); + if ( + typeof message.method !== "string" || + message.method.length === 0 || + hasResult || + hasError + ) { + this.#fatal( + new Error("codex app-server emitted a malformed JSON-RPC message"), + ); return; } if (message.params !== undefined && !isRecord(message.params)) { - this.#fatal(new Error("codex app-server message params were not an object")); + this.#fatal( + new Error("codex app-server message params were not an object"), + ); return; } const params = message.params ?? {}; if (message.id !== undefined) { if (!isValidServerRequestId(message.id)) { - this.#fatal(new Error("codex app-server request id had an invalid type")); + this.#fatal( + new Error("codex app-server request id had an invalid type"), + ); return; } - this.#handleServerRequest({ id: message.id, method: message.method, params }); + this.#handleServerRequest({ + id: message.id, + method: message.method, + params, + }); return; } if (!this.#notifications.push({ method: message.method, params }, bytes)) { - this.#fatal(new Error("codex app-server notification queue limit exceeded")); + this.#fatal( + new Error("codex app-server notification queue limit exceeded"), + ); } } @@ -533,26 +637,34 @@ export class ProcessCodexAppServerTransport implements CodexAppServerTransport { if (this.#inflightServerRequests >= this.#maxInflightServerRequests) { this.#write({ id: request.id, - error: { code: -32_001, message: "Client request queue limit exceeded." }, + error: { + code: -32_001, + message: "Client request queue limit exceeded.", + }, }); return; } this.#inflightServerRequests += 1; - void this.#serverRequestHandler(request).then( - (result) => { - if (!this.#closed) this.#write({ id: request.id, result }); - }, - (error: unknown) => { - if (!this.#closed) { - this.#write({ - id: request.id, - error: { code: -32_000, message: redactCodexDiagnostic(String(error)) }, - }); - } - }, - ).finally(() => { - this.#inflightServerRequests -= 1; - }); + void this.#serverRequestHandler(request) + .then( + (result) => { + if (!this.#closed) this.#write({ id: request.id, result }); + }, + (error: unknown) => { + if (!this.#closed) { + this.#write({ + id: request.id, + error: { + code: -32_000, + message: redactCodexDiagnostic(String(error)), + }, + }); + } + }, + ) + .finally(() => { + this.#inflightServerRequests -= 1; + }); } #fatal(error: Error): void { @@ -583,7 +695,9 @@ function positiveLimit(value: number | undefined, fallback: number): number { function isValidServerRequestId(value: unknown): value is string | number { return ( (typeof value === "number" && Number.isSafeInteger(value)) || - (typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= 256) + (typeof value === "string" && + value.length > 0 && + Buffer.byteLength(value) <= 256) ); } diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts index 68f0ca26520..9ea186f6151 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts @@ -42,6 +42,7 @@ import { type CodexRpcNotification, type CodexRpcServerRequest, type CodexServerRequestHandler, + type CodexTraceInterpretation, } from "./app-server-transport.js"; class TestQueue implements AsyncIterable { @@ -57,7 +58,8 @@ class TestQueue implements AsyncIterable { close(): void { this.closed = true; - for (const waiter of this.waiters.splice(0)) waiter({ value: undefined, done: true }); + for (const waiter of this.waiters.splice(0)) + waiter({ value: undefined, done: true }); } [Symbol.asyncIterator](): AsyncIterator { @@ -73,8 +75,13 @@ class TestQueue implements AsyncIterable { } class FakeCodexTransport implements CodexAppServerTransport { - readonly calls: Array<{ method: string; params: Record }> = []; - readonly sentNotifications: Array<{ method: string; params?: Record }> = []; + readonly calls: Array<{ method: string; params: Record }> = + []; + readonly sentNotifications: Array<{ + method: string; + params?: Record; + }> = []; + readonly traceInterpretations: CodexTraceInterpretation[] = []; readonly queue = new TestQueue(); handler: CodexServerRequestHandler = async () => ({}); rejectMethods = new Map(); @@ -88,20 +95,38 @@ class FakeCodexTransport implements CodexAppServerTransport { readonly providerSessionId = "provider-session-1", ) {} - async request(method: string, params: Record): Promise> { + async request( + method: string, + params: Record, + ): Promise> { this.calls.push({ method, params: structuredClone(params) }); const rejection = this.rejectMethods.get(method); if (rejection) throw rejection; if (method === "initialize") { - return { userAgent: "codex-cli/0.132.0", codexHome: "/isolated/codex", platformFamily: "unix", platformOs: "linux" }; + return { + userAgent: "codex-cli/0.132.0", + codexHome: "/isolated/codex", + platformFamily: "unix", + platformOs: "linux", + }; } if (method === "collaborationMode/list") { return this.confirmCollaborationMode - ? { data: [{ name: "Plan", mode: "plan", model: "gpt-test", reasoning_effort: "high" }] } + ? { + data: [ + { + name: "Plan", + mode: "plan", + model: "gpt-test", + reasoning_effort: "high", + }, + ], + } : { data: [{ name: "Default", mode: "default", model: "gpt-test" }] }; } if (method === "thread/start" || method === "thread/resume") { - const planMode = params.permissions === "paperclip-runner-workspace-read-only"; + const planMode = + params.permissions === "paperclip-runner-workspace-read-only"; return { thread: { id: this.threadId, @@ -109,7 +134,11 @@ class FakeCodexTransport implements CodexAppServerTransport { modelProvider: "openai", cwd: "/workspace", turns: [], - activePermissionProfile: { id: planMode ? "paperclip-runner-workspace-read-only" : "paperclip-runner-workspace-only" }, + activePermissionProfile: { + id: planMode + ? "paperclip-runner-workspace-read-only" + : "paperclip-runner-workspace-only", + }, }, model: "gpt-test", modelProvider: "openai", @@ -120,15 +149,22 @@ class FakeCodexTransport implements CodexAppServerTransport { }; } if (method === "turn/start") { - return this.turnStartResponse ?? { turn: { id: "turn-1", status: "inProgress", items: [] } }; + return ( + this.turnStartResponse ?? { + turn: { id: "turn-1", status: "inProgress", items: [] }, + } + ); } if (method === "thread/goal/get") return { goal: this.goalState }; if (method === "thread/goal/set") { this.goalState = { threadId: this.threadId, - objective: typeof params.objective === "string" - ? params.objective - : String(this.goalState?.objective ?? "Ship the Live console tracer"), + objective: + typeof params.objective === "string" + ? params.objective + : String( + this.goalState?.objective ?? "Ship the Live console tracer", + ), status: params.status ?? this.goalState?.status ?? "active", tokenBudget: params.tokenBudget ?? this.goalState?.tokenBudget ?? null, tokensUsed: 0, @@ -143,13 +179,24 @@ class FakeCodexTransport implements CodexAppServerTransport { return {}; } if (method === "thread/read") { - return this.readResponse ?? { thread: { id: this.threadId, sessionId: this.providerSessionId, cwd: "/workspace", turns: [{ id: "turn-1", status: "inProgress", items: [] }] } }; + return ( + this.readResponse ?? { + thread: { + id: this.threadId, + sessionId: this.providerSessionId, + cwd: "/workspace", + turns: [{ id: "turn-1", status: "inProgress", items: [] }], + }, + } + ); } return {}; } notify(method: string, params?: Record): void { - this.sentNotifications.push(params === undefined ? { method } : { method, params }); + this.sentNotifications.push( + params === undefined ? { method } : { method, params }, + ); } notifications(): AsyncIterable { @@ -160,6 +207,10 @@ class FakeCodexTransport implements CodexAppServerTransport { this.handler = handler; } + recordTraceInterpretation(input: CodexTraceInterpretation): void { + this.traceInterpretations.push(structuredClone(input)); + } + async close(): Promise { this.queue.close(); } @@ -168,6 +219,19 @@ class FakeCodexTransport implements CodexAppServerTransport { this.queue.push({ method, params }); } + pushTraced( + method: string, + params: Record, + sourceEventId: string, + sourceEventType: string, + ): void { + this.queue.push({ + method, + params, + paperclipTrace: { sourceEventId, sourceEventType }, + }); + } + invoke(request: CodexRpcServerRequest): Promise> { return this.handler(request); } @@ -178,10 +242,12 @@ const envelope = createCodexTaskEnvelope({ criteria: [{ id: "file", requirement: "hello.txt contains hello" }], }); -const liveConsoleFixturePath = fileURLToPath(new URL( - "../../../protocol/fixtures/codex-driver/driver-conformance.json", - import.meta.url, -)); +const liveConsoleFixturePath = fileURLToPath( + new URL( + "../../../protocol/fixtures/codex-driver/driver-conformance.json", + import.meta.url, + ), +); const result: PrpStructuredRunResult = { schema: "paperclip.run_result.v1", @@ -190,7 +256,9 @@ const result: PrpStructuredRunResult = { completionClaim: { contractRevision: "codex-demo-v1", objectiveSatisfied: true, - criteria: [{ criterionId: "file", status: "satisfied", evidenceRefs: ["hello.txt"] }], + criteria: [ + { criterionId: "file", status: "satisfied", evidenceRefs: ["hello.txt"] }, + ], remainingWork: [], }, evidence: [{ ref: "hello.txt" }], @@ -199,7 +267,10 @@ const result: PrpStructuredRunResult = { artifacts: [{ kind: "file", ref: "hello.txt" }], }; -function makeDriver(transports: FakeCodexTransport[], options: Record = {}) { +function makeDriver( + transports: FakeCodexTransport[], + options: Record = {}, +) { let index = 0; return new CodexAppServerDriver({ taskEnvelope: envelope, @@ -217,11 +288,21 @@ function makeDriver(transports: FakeCodexTransport[], options: Record): Promise { +async function collectUntilTerminal( + events: AsyncIterable, +): Promise { const collected: PrpEvent[] = []; for await (const event of events) { collected.push(event); - if (["turn.completed", "turn.failed", "turn.interrupted", "turn.cancelled"].includes(event.eventType)) break; + if ( + [ + "turn.completed", + "turn.failed", + "turn.interrupted", + "turn.cancelled", + ].includes(event.eventType) + ) + break; } return collected; } @@ -247,7 +328,9 @@ async function traceCompletedProposal( } = {}, ) { const transport = new FakeCodexTransport(); - const driver = makeDriver([transport], { capabilities: options.capabilities }); + const driver = makeDriver([transport], { + capabilities: options.capabilities, + }); const traced = runCodexCodexTracer({ driver, taskEnvelope: envelope, @@ -268,7 +351,11 @@ async function traceCompletedProposal( transport.push("item/completed", { threadId: "thread-1", turnId: "turn-1", - item: { id: "answer-1", type: "agentMessage", text: JSON.stringify(proposal) }, + item: { + id: "answer-1", + type: "agentMessage", + text: JSON.stringify(proposal), + }, }); } transport.push("turn/completed", { @@ -325,12 +412,18 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); - const threadStart = transport.calls.find((call) => call.method === "thread/start"); + const threadStart = transport.calls.find( + (call) => call.method === "thread/start", + ); expect(threadStart?.params).not.toHaveProperty("baseInstructions"); expect(threadStart?.params.dynamicTools).toEqual([]); - const first = await session.startTurn({ message: { role: "user", text: "Hello Codex" } }); - const firstStart = transport.calls.find((call) => call.method === "turn/start"); + const first = await session.startTurn({ + message: { role: "user", text: "Hello Codex" }, + }); + const firstStart = transport.calls.find( + (call) => call.method === "turn/start", + ); expect(firstStart?.params).not.toHaveProperty("outputSchema"); expect(firstStart?.params.input).toEqual([ { type: "text", text: "Hello Codex", text_elements: [] }, @@ -360,18 +453,56 @@ describe("Codex app-server Codex driver", () => { const transport = new FakeCodexTransport(); const driver = makeDriver([transport]); const descriptor = await driver.descriptor(); - const session = await driver.openSession({ runId: "run-1", normalizedSessionId: "normalized-1", workingDirectory: "/workspace" }); - const turn = await session.startTurn({ message: { role: "user", text: "Do the safe task." } }); + const session = await driver.openSession({ + runId: "run-1", + normalizedSessionId: "normalized-1", + workingDirectory: "/workspace", + }); + const turn = await session.startTurn({ + message: { role: "user", text: "Do the safe task." }, + }); - transport.push("turn/started", { threadId: "thread-1", turn: { id: turn.turnId, status: "inProgress" } }); - transport.push("item/started", { threadId: "thread-1", turnId: turn.turnId, item: { id: "cmd-1", type: "commandExecution", command: "printf hello" } }); - transport.push("item/commandExecution/outputDelta", { threadId: "thread-1", turnId: turn.turnId, itemId: "cmd-1", delta: "hello" }); - transport.push("item/completed", { threadId: "thread-1", turnId: turn.turnId, item: { id: "file-1", type: "fileChange", changes: [{ path: "hello.txt" }] } }); - transport.push("item/completed", { threadId: "thread-1", turnId: turn.turnId, item: { id: "reference-1", type: "agentMessage", text: "Open [hello.txt](hello.txt)." } }); + transport.push("turn/started", { + threadId: "thread-1", + turn: { id: turn.turnId, status: "inProgress" }, + }); + transport.push("item/started", { + threadId: "thread-1", + turnId: turn.turnId, + item: { id: "cmd-1", type: "commandExecution", command: "printf hello" }, + }); + transport.push("item/commandExecution/outputDelta", { + threadId: "thread-1", + turnId: turn.turnId, + itemId: "cmd-1", + delta: "hello", + }); + transport.push("item/completed", { + threadId: "thread-1", + turnId: turn.turnId, + item: { + id: "file-1", + type: "fileChange", + changes: [{ path: "hello.txt" }], + }, + }); + transport.push("item/completed", { + threadId: "thread-1", + turnId: turn.turnId, + item: { + id: "reference-1", + type: "agentMessage", + text: "Open [hello.txt](hello.txt).", + }, + }); const requestResolution = transport.invoke({ id: "request-1", method: "item/tool/requestUserInput", - params: { threadId: "thread-1", turnId: turn.turnId, itemId: "question-1" }, + params: { + threadId: "thread-1", + turnId: turn.turnId, + itemId: "question-1", + }, }); await new Promise((resolve) => setTimeout(resolve, 0)); expect(session.pendingRuntimeRequests?.()).toHaveLength(1); @@ -381,25 +512,73 @@ describe("Codex app-server Codex driver", () => { resolution: { action: "cancel" }, }); expect(await requestResolution).toEqual({ answers: {} }); - transport.push("thread/tokenUsage/updated", { threadId: "thread-1", turnId: turn.turnId, tokenUsage: { total: { inputTokens: 10, outputTokens: 4 }, modelContextWindow: 128000 } }); - transport.push("item/completed", { threadId: "thread-1", turnId: turn.turnId, item: { id: "answer-1", type: "agentMessage", text: JSON.stringify(result) } }); - transport.push("turn/completed", { threadId: "thread-1", turn: { id: turn.turnId, status: "completed", items: [] } }); + transport.push("thread/tokenUsage/updated", { + threadId: "thread-1", + turnId: turn.turnId, + tokenUsage: { + total: { inputTokens: 10, outputTokens: 4 }, + modelContextWindow: 128000, + }, + }); + transport.push("item/completed", { + threadId: "thread-1", + turnId: turn.turnId, + item: { + id: "answer-1", + type: "agentMessage", + text: JSON.stringify(result), + }, + }); + transport.push("turn/completed", { + threadId: "thread-1", + turn: { id: turn.turnId, status: "completed", items: [] }, + }); const events = await collectUntilTerminal(session.events()); expect(events.every((event) => validatePrpEvent(event).ok)).toBe(true); - expect(events.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(1); - expect(events.filter((event) => event.eventType === "turn.completed")).toHaveLength(1); - expect(events.filter((event) => event.eventType === "run.terminal")).toHaveLength(0); - expect(events.find((event) => event.eventType === "workspace.change.updated")?.payload) - .toMatchObject({ schema: "paperclip.workspace.diff.v1", complete: true, totals: { files: 1 } }); - expect(events.find((event) => event.eventType === "workspace.diff.recorded")?.payload) - .toMatchObject({ schema: "paperclip.workspace.diff.v1", complete: true }); - expect(events.find((event) => event.eventType === "workspace.file.referenced")?.payload) - .toMatchObject({ schema: "paperclip.workspace.file_reference.v1", path: "hello.txt" }); - expect(events.map((event) => event.eventType)).toEqual(expect.arrayContaining([ - "session.started", "turn.started", "item.started", "item.delta", "item.completed", "run.result.proposed", "turn.completed", - "runtime_request.created", "runtime_request.resolved", "workspace.change.updated", "workspace.diff.recorded", - ])); + expect( + events.filter((event) => event.eventType === "run.result.proposed"), + ).toHaveLength(1); + expect( + events.filter((event) => event.eventType === "turn.completed"), + ).toHaveLength(1); + expect( + events.filter((event) => event.eventType === "run.terminal"), + ).toHaveLength(0); + expect( + events.find((event) => event.eventType === "workspace.change.updated") + ?.payload, + ).toMatchObject({ + schema: "paperclip.workspace.diff.v1", + complete: true, + totals: { files: 1 }, + }); + expect( + events.find((event) => event.eventType === "workspace.diff.recorded") + ?.payload, + ).toMatchObject({ schema: "paperclip.workspace.diff.v1", complete: true }); + expect( + events.find((event) => event.eventType === "workspace.file.referenced") + ?.payload, + ).toMatchObject({ + schema: "paperclip.workspace.file_reference.v1", + path: "hello.txt", + }); + expect(events.map((event) => event.eventType)).toEqual( + expect.arrayContaining([ + "session.started", + "turn.started", + "item.started", + "item.delta", + "item.completed", + "run.result.proposed", + "turn.completed", + "runtime_request.created", + "runtime_request.resolved", + "workspace.change.updated", + "workspace.diff.recorded", + ]), + ); const capabilities: PrpCapabilities = { schema: "paperclip.prp.capabilities.v1", @@ -414,14 +593,86 @@ describe("Codex app-server Codex driver", () => { }; const metadata = { fixtureName: "codex-conformance", - identity: { schema: "paperclip.prp.identity.v1" as const, companyId: "company-1", issueId: "issue-1", runId: "run-1", environmentLeaseId: "lease-1", runnerInstanceId: "runner-codex", normalizedSessionId: "normalized-1" }, + identity: { + schema: "paperclip.prp.identity.v1" as const, + companyId: "company-1", + issueId: "issue-1", + runId: "run-1", + environmentLeaseId: "lease-1", + runnerInstanceId: "runner-codex", + normalizedSessionId: "normalized-1", + }, capabilities, }; - const live = events.reduce(applyPrpEvent, createSessionSnapshotFromMetadata(metadata)); - const replay = events.reduce(applyPrpEvent, createSessionSnapshotFromMetadata(metadata)); + const live = events.reduce( + applyPrpEvent, + createSessionSnapshotFromMetadata(metadata), + ); + const replay = events.reduce( + applyPrpEvent, + createSessionSnapshotFromMetadata(metadata), + ); expect(live).toEqual(replay); expect(live.integrity).toBe("complete"); - expect(await session.usage?.()).toMatchObject({ modelContextWindow: 128000 }); + expect(await session.usage?.()).toMatchObject({ + modelContextWindow: 128000, + }); + }); + + it("correlates rehydrated notifications with the final canonical PRP event ids", async () => { + const transport = new FakeCodexTransport(); + const session = await makeDriver([transport]).openSession({ + runId: "run-traced", + normalizedSessionId: "normalized-traced", + workingDirectory: "/workspace", + }); + const turn = await session.startTurn({ + message: { role: "user", text: "Trace the response." }, + }); + + transport.pushTraced( + "turn/started", + { + threadId: "thread-1", + turn: { id: turn.turnId, status: "inProgress" }, + }, + "event_runner_000001", + "turn.started", + ); + transport.pushTraced( + "item/started", + { + threadId: "thread-1", + turnId: turn.turnId, + item: { id: "answer-1", type: "agentMessage", phase: "final_answer" }, + }, + "event_runner_000002", + "item.started", + ); + transport.pushTraced( + "turn/completed", + { + threadId: "thread-1", + turn: { id: turn.turnId, status: "completed", items: [] }, + }, + "event_runner_000003", + "turn.completed", + ); + + const events = await collectUntilTerminal(session.events()); + const itemMapping = transport.traceInterpretations.find( + (entry) => entry.providerMethod === "item/started", + ); + expect(itemMapping).toMatchObject({ + sourceEventId: "event_runner_000002", + sourceEventType: "item.started", + disposition: "mapped", + }); + expect(itemMapping?.emittedEventIds.length).toBeGreaterThan(0); + expect(events.map((event) => event.sourceEventId)).toEqual( + expect.arrayContaining(itemMapping?.emittedEventIds ?? []), + ); + expect(JSON.stringify(events)).not.toContain("paperclipTrace"); }); it("accepts a thread usage snapshot replayed before a resumed turn starts", async () => { @@ -437,7 +688,9 @@ describe("Codex app-server Codex driver", () => { turnId: "prior-turn", tokenUsage: { total: { inputTokens: 10, outputTokens: 4 } }, }); - const turn = await session.startTurn({ message: { role: "user", text: "Continue." } }); + const turn = await session.startTurn({ + message: { role: "user", text: "Continue." }, + }); transport.push("turn/started", { threadId: "thread-1", turn: { id: turn.turnId, status: "inProgress" }, @@ -448,8 +701,12 @@ describe("Codex app-server Codex driver", () => { }); const events = await collectUntilTerminal(session.events()); - expect(events.some((event) => event.eventType === "session.failed")).toBe(false); - expect(await session.usage()).toMatchObject({ total: { inputTokens: 10, outputTokens: 4 } }); + expect(events.some((event) => event.eventType === "session.failed")).toBe( + false, + ); + expect(await session.usage()).toMatchObject({ + total: { inputTokens: 10, outputTokens: 4 }, + }); }); it("normalizes provider message and reasoning phases onto every streamed item event", async () => { @@ -459,39 +716,146 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "normalized-stream-channels", workingDirectory: "/workspace", }); - const turn = await session.startTurn({ message: { role: "user", text: "Stream progress." } }); - transport.push("turn/started", { threadId: "thread-1", turn: { id: turn.turnId, status: "inProgress" } }); - transport.push("item/started", { threadId: "thread-1", turnId: turn.turnId, item: { id: "progress-1", type: "agentMessage", phase: "commentary", text: "" } }); - transport.push("item/agentMessage/delta", { threadId: "thread-1", turnId: turn.turnId, itemId: "progress-1", delta: "Running it now." }); - transport.push("item/completed", { threadId: "thread-1", turnId: turn.turnId, item: { id: "progress-1", type: "agentMessage", phase: "commentary", text: "Running it now." } }); - transport.push("item/started", { threadId: "thread-1", turnId: turn.turnId, item: { id: "reason-1", type: "reasoning", summary: [] } }); - transport.push("item/reasoning/summaryTextDelta", { threadId: "thread-1", turnId: turn.turnId, itemId: "reason-1", delta: "Summary" }); - transport.push("item/reasoning/textDelta", { threadId: "thread-1", turnId: turn.turnId, itemId: "reason-1", delta: "Detail" }); - transport.push("item/completed", { threadId: "thread-1", turnId: turn.turnId, item: { id: "reason-1", type: "reasoning", summary: [{ text: "Summary" }] } }); - transport.push("item/started", { threadId: "thread-1", turnId: turn.turnId, item: { id: "final-1", type: "agentMessage", phase: "final_answer", text: "" } }); - transport.push("item/agentMessage/delta", { threadId: "thread-1", turnId: turn.turnId, itemId: "final-1", delta: JSON.stringify(result) }); - transport.push("item/completed", { threadId: "thread-1", turnId: turn.turnId, item: { id: "final-1", type: "agentMessage", phase: "final_answer", text: JSON.stringify(result) } }); - transport.push("turn/completed", { threadId: "thread-1", turn: { id: turn.turnId, status: "completed", items: [] } }); + const turn = await session.startTurn({ + message: { role: "user", text: "Stream progress." }, + }); + transport.push("turn/started", { + threadId: "thread-1", + turn: { id: turn.turnId, status: "inProgress" }, + }); + transport.push("item/started", { + threadId: "thread-1", + turnId: turn.turnId, + item: { + id: "progress-1", + type: "agentMessage", + phase: "commentary", + text: "", + }, + }); + transport.push("item/agentMessage/delta", { + threadId: "thread-1", + turnId: turn.turnId, + itemId: "progress-1", + delta: "Running it now.", + }); + transport.push("item/completed", { + threadId: "thread-1", + turnId: turn.turnId, + item: { + id: "progress-1", + type: "agentMessage", + phase: "commentary", + text: "Running it now.", + }, + }); + transport.push("item/started", { + threadId: "thread-1", + turnId: turn.turnId, + item: { id: "reason-1", type: "reasoning", summary: [] }, + }); + transport.push("item/reasoning/summaryTextDelta", { + threadId: "thread-1", + turnId: turn.turnId, + itemId: "reason-1", + delta: "Summary", + }); + transport.push("item/reasoning/textDelta", { + threadId: "thread-1", + turnId: turn.turnId, + itemId: "reason-1", + delta: "Detail", + }); + transport.push("item/completed", { + threadId: "thread-1", + turnId: turn.turnId, + item: { + id: "reason-1", + type: "reasoning", + summary: [{ text: "Summary" }], + }, + }); + transport.push("item/started", { + threadId: "thread-1", + turnId: turn.turnId, + item: { + id: "final-1", + type: "agentMessage", + phase: "final_answer", + text: "", + }, + }); + transport.push("item/agentMessage/delta", { + threadId: "thread-1", + turnId: turn.turnId, + itemId: "final-1", + delta: JSON.stringify(result), + }); + transport.push("item/completed", { + threadId: "thread-1", + turnId: turn.turnId, + item: { + id: "final-1", + type: "agentMessage", + phase: "final_answer", + text: JSON.stringify(result), + }, + }); + transport.push("turn/completed", { + threadId: "thread-1", + turn: { id: turn.turnId, status: "completed", items: [] }, + }); const events = await collectUntilTerminal(session.events()); - const pick = (kind: string, channel: string) => events.find((event) => - event.payload.kind === kind && event.payload.channel === channel); - expect(pick("agentMessage", "progress")?.payload).toMatchObject({ providerPhase: "commentary" }); - expect(events.find((event) => event.eventType === "item.delta" && event.payload.channel === "progress")?.payload) - .toMatchObject({ providerMethod: "item/agentMessage/delta", text: "Running it now." }); - expect(events.find((event) => event.eventType === "item.delta" && event.payload.channel === "summary")?.payload) - .toMatchObject({ providerMethod: "item/reasoning/summaryTextDelta" }); - expect(events.find((event) => event.eventType === "item.delta" && event.payload.channel === "detail")?.payload) - .toMatchObject({ providerMethod: "item/reasoning/textDelta" }); - expect(pick("agentMessage", "final")?.payload).toMatchObject({ providerPhase: "final_answer" }); + const pick = (kind: string, channel: string) => + events.find( + (event) => + event.payload.kind === kind && event.payload.channel === channel, + ); + expect(pick("agentMessage", "progress")?.payload).toMatchObject({ + providerPhase: "commentary", + }); + expect( + events.find( + (event) => + event.eventType === "item.delta" && + event.payload.channel === "progress", + )?.payload, + ).toMatchObject({ + providerMethod: "item/agentMessage/delta", + text: "Running it now.", + }); + expect( + events.find( + (event) => + event.eventType === "item.delta" && + event.payload.channel === "summary", + )?.payload, + ).toMatchObject({ providerMethod: "item/reasoning/summaryTextDelta" }); + expect( + events.find( + (event) => + event.eventType === "item.delta" && + event.payload.channel === "detail", + )?.payload, + ).toMatchObject({ providerMethod: "item/reasoning/textDelta" }); + expect(pick("agentMessage", "final")?.payload).toMatchObject({ + providerPhase: "final_answer", + }); }); it("captures an exact skillless model/environment snapshot with credentials absent", async () => { const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ runId: "run-context", normalizedSessionId: "normalized-context", workingDirectory: "/workspace" }); + const session = await makeDriver([transport]).openSession({ + runId: "run-context", + normalizedSessionId: "normalized-context", + workingDirectory: "/workspace", + }); const iterator = session.events()[Symbol.asyncIterator](); const first = await iterator.next(); - const context = first.value?.payload.context as Parameters[0]; + const context = first.value?.payload.context as Parameters< + typeof isSkilllessCodexContext + >[0]; expect(context).toMatchObject({ codexVersion: "codex-cli/0.132.0", model: "gpt-test", @@ -502,7 +866,7 @@ describe("Codex app-server Codex driver", () => { instructionPolicy: { skillInstructions: false, appInstructions: false, - collaborationInstructions: false, + collaborationInstructions: true, }, environmentKeys: ["LANG", "PATH"], envelope, @@ -510,20 +874,23 @@ describe("Codex app-server Codex driver", () => { expect(isSkilllessCodexContext(context)).toBe(true); expect(JSON.stringify(context)).not.toContain("must-not-pass"); expect(JSON.stringify(transport.calls)).not.toContain("RANDOM_SKILL_PATH"); - expect(transport.calls.find((call) => call.method === "thread/start")?.params).toMatchObject({ + expect( + transport.calls.find((call) => call.method === "thread/start")?.params, + ).toMatchObject({ approvalPolicy: "never", config: { "skills.include_instructions": false, include_apps_instructions: false, - include_collaboration_mode_instructions: false, + include_collaboration_mode_instructions: true, "features.image_generation": false, }, permissions: "paperclip-runner-workspace-only", runtimeWorkspaceRoots: ["/workspace"], dynamicTools: [{ name: "paperclip_finish" }, { name: "paperclip_block" }], }); - const commandPolicy = transport.calls.find((call) => call.method === "thread/start") - ?.params.config as Record; + const commandPolicy = transport.calls.find( + (call) => call.method === "thread/start", + )?.params.config as Record; expect(JSON.stringify(commandPolicy)).not.toContain("/isolated/home"); expect(JSON.stringify(commandPolicy)).not.toContain("/isolated/codex"); expect(JSON.stringify(commandPolicy)).not.toContain("CODEX_HOME"); @@ -533,48 +900,94 @@ describe("Codex app-server Codex driver", () => { HOME: "/isolated/home", CODEX_HOME: "/isolated/codex", }); - expect(appServerArgs).toEqual(expect.arrayContaining([ - expect.stringContaining('default_permissions="paperclip-runner-workspace-only"'), - expect.stringContaining('permissions.paperclip-runner-workspace-only.filesystem='), - "permissions.paperclip-runner-workspace-only.network.enabled=false", - 'shell_environment_policy.inherit="none"', - expect.stringContaining('shell_environment_policy.set={PATH="/bin",LANG="C.UTF-8"}'), - "--disable", - "image_generation", - ])); - expect(appServerArgs.find((arg) => arg.startsWith("permissions."))).toContain( - '"/isolated/home"="none"', - ); - expect(appServerArgs.find((arg) => arg.startsWith("permissions."))).toContain( - '"/isolated/codex"="none"', + expect(appServerArgs).toEqual( + expect.arrayContaining([ + expect.stringContaining( + 'default_permissions="paperclip-runner-workspace-only"', + ), + expect.stringContaining( + "permissions.paperclip-runner-workspace-only.filesystem=", + ), + "permissions.paperclip-runner-workspace-only.network.enabled=false", + 'shell_environment_policy.inherit="none"', + expect.stringContaining( + 'shell_environment_policy.set={PATH="/bin",LANG="C.UTF-8"}', + ), + "--disable", + "image_generation", + ]), ); + expect( + appServerArgs.find((arg) => arg.startsWith("permissions.")), + ).toContain('"/isolated/home"="none"'); + expect( + appServerArgs.find((arg) => arg.startsWith("permissions.")), + ).toContain('"/isolated/codex"="none"'); expect(JSON.stringify(appServerArgs)).not.toContain("HOME="); expect(CODEX_RESULT_OUTPUT_SCHEMA.properties.schema).toEqual({ type: "string", const: "paperclip.run_result.v1", }); - expect(CODEX_BLOCK_RESULT_OUTPUT_SCHEMA.properties.reportedWorkDisposition).toEqual({ + expect( + CODEX_BLOCK_RESULT_OUTPUT_SCHEMA.properties.reportedWorkDisposition, + ).toEqual({ type: "string", const: "blocked", }); }); + it("allows eval fixtures to opt out of Codex collaboration instructions", async () => { + const transport = new FakeCodexTransport(); + const session = await makeDriver([transport], { + includeCollaborationModeInstructions: false, + }).openSession({ + runId: "run-no-collaboration-instructions", + normalizedSessionId: "session-no-collaboration-instructions", + workingDirectory: "/workspace", + }); + const first = await session.events()[Symbol.asyncIterator]().next(); + + expect(first.value?.payload.context).toMatchObject({ + instructionPolicy: { collaborationInstructions: false }, + }); + expect( + transport.calls.find((call) => call.method === "thread/start")?.params, + ).toMatchObject({ + config: { include_collaboration_mode_instructions: false }, + }); + }); + it("negotiates genuine plan mode with collaboration instructions and read-only workspace access", async () => { const transport = new FakeCodexTransport(); - const driver = makeDriver([transport], { requestedCollaborationMode: "plan" }); - const session = await driver.openSession({ runId: "run-plan", normalizedSessionId: "session-plan", workingDirectory: "/workspace" }); - expect(transport.calls.find((call) => call.method === "thread/start")?.params).toMatchObject({ + const driver = makeDriver([transport], { + requestedCollaborationMode: "plan", + }); + const session = await driver.openSession({ + runId: "run-plan", + normalizedSessionId: "session-plan", + workingDirectory: "/workspace", + }); + expect( + transport.calls.find((call) => call.method === "thread/start")?.params, + ).toMatchObject({ permissions: "paperclip-runner-workspace-read-only", config: { include_collaboration_mode_instructions: true }, }); - expect(transport.calls.find((call) => call.method === "thread/start")?.params) - .not.toHaveProperty("collaborationMode"); - expect(transport.calls.some((call) => call.method === "collaborationMode/list")).toBe(true); - await expect(session.startTurn({ - message: { role: "user", text: "Author a plan." }, - requestedCollaborationMode: "plan", - })).resolves.toMatchObject({ effectiveCollaborationMode: "plan" }); - expect(transport.calls.find((call) => call.method === "turn/start")?.params).toMatchObject({ + expect( + transport.calls.find((call) => call.method === "thread/start")?.params, + ).not.toHaveProperty("collaborationMode"); + expect( + transport.calls.some((call) => call.method === "collaborationMode/list"), + ).toBe(true); + await expect( + session.startTurn({ + message: { role: "user", text: "Author a plan." }, + requestedCollaborationMode: "plan", + }), + ).resolves.toMatchObject({ effectiveCollaborationMode: "plan" }); + expect( + transport.calls.find((call) => call.method === "turn/start")?.params, + ).toMatchObject({ collaborationMode: { mode: "plan", settings: { @@ -590,46 +1003,78 @@ describe("Codex app-server Codex driver", () => { it("fails closed when the installed app-server does not confirm plan mode", async () => { const transport = new FakeCodexTransport(); transport.confirmCollaborationMode = false; - await expect(makeDriver([transport], { requestedCollaborationMode: "plan" }).openSession({ - runId: "run-plan-unsupported", - normalizedSessionId: "session-plan-unsupported", - workingDirectory: "/workspace", - })).rejects.toThrow("planning_mode_unsupported"); + await expect( + makeDriver([transport], { + requestedCollaborationMode: "plan", + }).openSession({ + runId: "run-plan-unsupported", + normalizedSessionId: "session-plan-unsupported", + workingDirectory: "/workspace", + }), + ).rejects.toThrow("planning_mode_unsupported"); }); it("refuses to turn host credential roots into model-writable workspaces", async () => { - await expect(makeDriver([]).openSession({ - runId: "run-codex-home", - normalizedSessionId: "normalized-codex-home", - workingDirectory: "/isolated/codex", - })).rejects.toThrow("cannot overlap host CODEX_HOME"); - await expect(makeDriver([]).openSession({ - runId: "run-host-home", - normalizedSessionId: "normalized-host-home", - workingDirectory: "/isolated", - })).rejects.toThrow("cannot contain the host HOME"); - await expect(makeDriver([]).openSession({ - runId: "run-root", - normalizedSessionId: "normalized-root", - workingDirectory: "/", - })).rejects.toThrow("cannot be a filesystem root"); + await expect( + makeDriver([]).openSession({ + runId: "run-codex-home", + normalizedSessionId: "normalized-codex-home", + workingDirectory: "/isolated/codex", + }), + ).rejects.toThrow("cannot overlap host CODEX_HOME"); + await expect( + makeDriver([]).openSession({ + runId: "run-host-home", + normalizedSessionId: "normalized-host-home", + workingDirectory: "/isolated", + }), + ).rejects.toThrow("cannot contain the host HOME"); + await expect( + makeDriver([]).openSession({ + runId: "run-root", + normalizedSessionId: "normalized-root", + workingDirectory: "/", + }), + ).rejects.toThrow("cannot be a filesystem root"); }); it("steers and interrupts an active turn without replacing the session", async () => { const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ runId: "run-controls", normalizedSessionId: "normalized-controls", workingDirectory: "/workspace" }); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Start." } }); - await session.steer?.({ turnId, message: { role: "user", text: "Use a shorter answer." } }); + const session = await makeDriver([transport]).openSession({ + runId: "run-controls", + normalizedSessionId: "normalized-controls", + workingDirectory: "/workspace", + }); + const { turnId } = await session.startTurn({ + message: { role: "user", text: "Start." }, + }); + await session.steer?.({ + turnId, + message: { role: "user", text: "Use a shorter answer." }, + }); await session.interrupt?.({ turnId, reason: "operator requested" }); expect(transport.calls.map((call) => call.method)).toEqual([ - "initialize", "thread/start", "thread/goal/get", "turn/start", "turn/steer", "turn/interrupt", + "initialize", + "thread/start", + "thread/goal/get", + "turn/start", + "turn/steer", + "turn/interrupt", ]); - expect(session.ids()).toEqual({ driverSessionId: "thread-1", providerSessionId: "provider-session-1", displayId: "thread-1" }); + expect(session.ids()).toEqual({ + driverSessionId: "thread-1", + providerSessionId: "provider-session-1", + displayId: "thread-1", + }); }); it("loads the deterministic Live console wire fixture", async () => { - const fixture = await loadLiveConsoleConformanceFixture(liveConsoleFixturePath); - expect(fixture.runtimeRequests.map(({ requestKind }) => requestKind)).toEqual([ + const fixture = await loadLiveConsoleConformanceFixture( + liveConsoleFixturePath, + ); + expect( + fixture.runtimeRequests.map(({ requestKind }) => requestKind), + ).toEqual([ "command_approval", "file_approval", "permission_approval", @@ -637,12 +1082,18 @@ describe("Codex app-server Codex driver", () => { "elicitation", ]); expect(fixture.goals.map(({ action }) => action)).toEqual([ - "get", "set", "pause", "resume", "clear", + "get", + "set", + "pause", + "resume", + "clear", ]); }); it("holds browser-resolved upstream requests and returns the exact fixture responses", async () => { - const fixture = await loadLiveConsoleConformanceFixture(liveConsoleFixturePath); + const fixture = await loadLiveConsoleConformanceFixture( + liveConsoleFixturePath, + ); for (const scenario of fixture.runtimeRequests) { const transport = new FakeCodexTransport(); const session = await makeDriver([transport]).openSession({ @@ -650,7 +1101,9 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: `normalized-${scenario.id}`, workingDirectory: "/workspace", }); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Exercise request." } }); + const { turnId } = await session.startTurn({ + message: { role: "user", text: "Exercise request." }, + }); const response = transport.invoke({ id: scenario.id, method: scenario.method, @@ -670,11 +1123,15 @@ describe("Codex app-server Codex driver", () => { turnId, }), ]); - await expect(session.resolveRuntimeRequest?.({ - requestId: scenario.id, - turnId, - resolution: { action: "forged" } as unknown as HarnessRuntimeRequestResolution, - })).rejects.toThrow("unsupported action"); + await expect( + session.resolveRuntimeRequest?.({ + requestId: scenario.id, + turnId, + resolution: { + action: "forged", + } as unknown as HarnessRuntimeRequestResolution, + }), + ).rejects.toThrow("unsupported action"); expect(session.pendingRuntimeRequests?.()).toHaveLength(1); await session.resolveRuntimeRequest?.({ requestId: scenario.id, @@ -683,24 +1140,30 @@ describe("Codex app-server Codex driver", () => { }); expect(await response).toEqual(scenario.expectedResponse); expect(session.pendingRuntimeRequests?.()).toEqual([]); - await expect(session.resolveRuntimeRequest?.({ - requestId: scenario.id, - turnId, - resolution: { action: "cancel" }, - })).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); + await expect( + session.resolveRuntimeRequest?.({ + requestId: scenario.id, + turnId, + resolution: { action: "cancel" }, + }), + ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); await session.close({ reason: "fixture complete" }); } }); it("acknowledges same-turn steering and rejects stale or child steering", async () => { - const fixture = await loadLiveConsoleConformanceFixture(liveConsoleFixturePath); + const fixture = await loadLiveConsoleConformanceFixture( + liveConsoleFixturePath, + ); const transport = new FakeCodexTransport("thread-root"); const session = await makeDriver([transport]).openSession({ runId: "run-live-console-controls", normalizedSessionId: "normalized-live-console-controls", workingDirectory: "/workspace", }); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Start." } }); + const { turnId } = await session.startTurn({ + message: { role: "user", text: "Start." }, + }); await session.steer?.({ turnId, message: { role: "user", text: "Stay concise." }, @@ -711,42 +1174,62 @@ describe("Codex app-server Codex driver", () => { message: { role: "user", text: "Stay concise." }, correlationId: "queued-comment-1", }); - expect(transport.calls.find((call) => call.method === "turn/steer")?.params) - .toMatchObject({ correlationId: "queued-comment-1" }); - expect(transport.calls.filter((call) => call.method === "turn/steer")).toHaveLength(1); - transport.rejectMethods.set("turn/steer", new Error("provider rejected steering")); - const rejectedSteering = await session.steer?.({ - turnId, - message: { role: "user", text: "Try this again later." }, - correlationId: "queued-comment-2", - }).then( - () => null, - (error: unknown) => error, + expect( + transport.calls.find((call) => call.method === "turn/steer")?.params, + ).toMatchObject({ correlationId: "queued-comment-1" }); + expect( + transport.calls.filter((call) => call.method === "turn/steer"), + ).toHaveLength(1); + transport.rejectMethods.set( + "turn/steer", + new Error("provider rejected steering"), ); + const rejectedSteering = await session + .steer?.({ + turnId, + message: { role: "user", text: "Try this again later." }, + correlationId: "queued-comment-2", + }) + .then( + () => null, + (error: unknown) => error, + ); expect(rejectedSteering).toBeInstanceOf(Error); - expect(rejectedSteering).not.toBeInstanceOf(HarnessCapabilityUnavailableError); + expect(rejectedSteering).not.toBeInstanceOf( + HarnessCapabilityUnavailableError, + ); expect(String(rejectedSteering)).toContain("provider rejected steering"); transport.rejectMethods.delete("turn/steer"); - await expect(session.steer?.({ - turnId: fixture.controls.staleTurnSteer.turnId!, - message: { role: "user", text: "Stale." }, - })).rejects.toBeInstanceOf(HarnessStaleTurnError); + await expect( + session.steer?.({ + turnId: fixture.controls.staleTurnSteer.turnId!, + message: { role: "user", text: "Stale." }, + }), + ).rejects.toBeInstanceOf(HarnessStaleTurnError); transport.push("thread/started", { thread: fixture.lineage.childThread }); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(session.lineage?.()).toEqual(expect.arrayContaining([ - expect.objectContaining({ threadId: "thread-root", parentThreadId: null, depth: 0 }), - expect.objectContaining({ - threadId: "thread-child", - parentThreadId: "thread-root", - depth: 1, - nickname: "Scout", - role: "researcher", + expect(session.lineage?.()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + threadId: "thread-root", + parentThreadId: null, + depth: 0, + }), + expect.objectContaining({ + threadId: "thread-child", + parentThreadId: "thread-root", + depth: 1, + nickname: "Scout", + role: "researcher", + }), + ]), + ); + await expect( + session.steer?.({ + turnId: "thread-child", + message: { role: "user", text: "Do not emulate child steering." }, }), - ])); - await expect(session.steer?.({ - turnId: "thread-child", - message: { role: "user", text: "Do not emulate child steering." }, - })).rejects.toBeInstanceOf(HarnessStaleTurnError); + ).rejects.toBeInstanceOf(HarnessStaleTurnError); const events = session.events()[Symbol.asyncIterator](); const observed: PrpEvent[] = []; for (let index = 0; index < 9; index += 1) { @@ -754,34 +1237,43 @@ describe("Codex app-server Codex driver", () => { if (next.done) break; observed.push(next.value); } - expect(observed).toEqual(expect.arrayContaining([ - expect.objectContaining({ - eventType: "item.completed", - itemId: `${turnId}:steer:queued-comment-1`, - payload: expect.objectContaining({ kind: "steering_acknowledgement", status: "acknowledged" }), - }), - expect.objectContaining({ - eventType: "harness.diagnostic", - payload: expect.objectContaining({ code: "stale_turn_rejected" }), - }), - expect.objectContaining({ - eventType: "item.started", - payload: expect.objectContaining({ kind: "thread_lineage" }), - }), - ])); + expect(observed).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + eventType: "item.completed", + itemId: `${turnId}:steer:queued-comment-1`, + payload: expect.objectContaining({ + kind: "steering_acknowledgement", + status: "acknowledged", + }), + }), + expect.objectContaining({ + eventType: "harness.diagnostic", + payload: expect.objectContaining({ code: "stale_turn_rejected" }), + }), + expect.objectContaining({ + eventType: "item.started", + payload: expect.objectContaining({ kind: "thread_lineage" }), + }), + ]), + ); await session.close({ reason: "fixture complete" }); }); it("queues an interrupt before turn identity and reports a terminal race precisely", async () => { const transport = new FakeCodexTransport(); let releaseStart!: (value: Record) => void; - transport.turnStartResponse = new Promise((resolve) => { releaseStart = resolve; }); + transport.turnStartResponse = new Promise((resolve) => { + releaseStart = resolve; + }); const session = await makeDriver([transport]).openSession({ runId: "run-interrupt-races", normalizedSessionId: "normalized-interrupt-races", workingDirectory: "/workspace", }); - const starting = session.startTurn({ message: { role: "user", text: "Start slowly." } }); + const starting = session.startTurn({ + message: { role: "user", text: "Start slowly." }, + }); await new Promise((resolve) => setTimeout(resolve, 0)); await session.interrupt?.({ reason: "operator" }); releaseStart({ turn: { id: "turn-1", status: "inProgress", items: [] } }); @@ -789,21 +1281,24 @@ describe("Codex app-server Codex driver", () => { turnId: "turn-1", effectiveCollaborationMode: "default", }); - expect(transport.calls.map(({ method }) => method)).toEqual(expect.arrayContaining([ - "turn/start", "turn/interrupt", - ])); + expect(transport.calls.map(({ method }) => method)).toEqual( + expect.arrayContaining(["turn/start", "turn/interrupt"]), + ); transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "interrupted", items: [] }, }); await new Promise((resolve) => setTimeout(resolve, 0)); - await expect(session.interrupt?.({ turnId: "turn-1" })) - .rejects.toBeInstanceOf(HarnessOperationAlreadyTerminalError); + await expect( + session.interrupt?.({ turnId: "turn-1" }), + ).rejects.toBeInstanceOf(HarnessOperationAlreadyTerminalError); await session.close({ reason: "fixture complete" }); }); it("maps all goal operations and advertises an exact unsupported state", async () => { - const fixture = await loadLiveConsoleConformanceFixture(liveConsoleFixturePath); + const fixture = await loadLiveConsoleConformanceFixture( + liveConsoleFixturePath, + ); const transport = new FakeCodexTransport(); const session = await makeDriver([transport]).openSession({ runId: "run-goals", @@ -811,23 +1306,37 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); await session.goal?.({ action: "get" }); - await session.goal?.({ action: "set", objective: "Ship the Live console tracer", tokenBudget: 4096 }); + await session.goal?.({ + action: "set", + objective: "Ship the Live console tracer", + tokenBudget: 4096, + }); await session.goal?.({ action: "pause" }); await session.goal?.({ action: "resume" }); await session.goal?.({ action: "clear" }); - const goalCalls = transport.calls.filter(({ method }) => method.startsWith("thread/goal/")); - expect(goalCalls.slice(1).map(({ method, params }) => ({ - method, - params: Object.fromEntries(Object.entries(params).filter(([key]) => key !== "threadId")), - }))).toEqual(fixture.goals.map(({ method, params }) => ({ method, params }))); + const goalCalls = transport.calls.filter(({ method }) => + method.startsWith("thread/goal/"), + ); + expect( + goalCalls.slice(1).map(({ method, params }) => ({ + method, + params: Object.fromEntries( + Object.entries(params).filter(([key]) => key !== "threadId"), + ), + })), + ).toEqual(fixture.goals.map(({ method, params }) => ({ method, params }))); - expect((await makeDriver([new FakeCodexTransport()]).descriptor()).capabilities) - .toMatchObject({ goals: true }); + expect( + (await makeDriver([new FakeCodexTransport()]).descriptor()).capabilities, + ).toMatchObject({ goals: true }); // Both denials a real app-server sends: the method is absent, and the // build has the feature switched off. for (const denial of [ - new CodexRpcError('{"code":-32601,"message":"method not found"}', CODEX_METHOD_NOT_FOUND), + new CodexRpcError( + '{"code":-32601,"message":"method not found"}', + CODEX_METHOD_NOT_FOUND, + ), new CodexRpcError( '{"code":-32600,"message":"goals feature is disabled"}', CODEX_INVALID_REQUEST, @@ -841,9 +1350,12 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "normalized-goals-unsupported", workingDirectory: "/workspace", }); - expect((await unsupportedDriver.descriptor()).capabilities).toMatchObject({ goals: false }); - await expect(unsupported.goal?.({ action: "get" })) - .rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); + expect((await unsupportedDriver.descriptor()).capabilities).toMatchObject( + { goals: false }, + ); + await expect( + unsupported.goal?.({ action: "get" }), + ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); await unsupported.close({ reason: "fixture complete" }); } await session.close({ reason: "fixture complete" }); @@ -865,12 +1377,16 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); - expect((await driver.descriptor()).capabilities).toMatchObject({ goals: true }); + expect((await driver.descriptor()).capabilities).toMatchObject({ + goals: true, + }); // The capability survives, so the operation is still offered and the // next call reaches the provider instead of failing closed locally. transport.rejectMethods.delete("thread/goal/get"); await expect(session.goal?.({ action: "get" })).resolves.toBeNull(); - expect(transport.calls.filter(({ method }) => method === "thread/goal/get")).toHaveLength(2); + expect( + transport.calls.filter(({ method }) => method === "thread/goal/get"), + ).toHaveLength(2); await session.close({ reason: "fixture complete" }); } }); @@ -882,15 +1398,25 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "normalized-resolution-shapes", workingDirectory: "/workspace", }); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Ask me things." } }); + const { turnId } = await session.startTurn({ + message: { role: "user", text: "Ask me things." }, + }); // Not `async`: an async function would flatten and await the pending // provider response, which only settles once the request is resolved. - function open(id: string, method: string): Promise> { + function open( + id: string, + method: string, + ): Promise> { return transport.invoke({ id, method, - params: { threadId: "thread-1", turnId, itemId: `item-${id}`, reason: `fixture ${id}` }, + params: { + threadId: "thread-1", + turnId, + itemId: `item-${id}`, + reason: `fixture ${id}`, + }, }); } const settled = () => new Promise((resolve) => setTimeout(resolve, 0)); @@ -905,22 +1431,32 @@ describe("Codex app-server Codex driver", () => { { action: "submit", content: { answer: "wrong shape" } }, { action: "accept" }, ]) { - await expect(session.resolveRuntimeRequest?.({ - requestId: "ask-input", - turnId, - resolution: resolution as unknown as HarnessRuntimeRequestResolution, - })).rejects.toThrow(/user_input rejected its resolution/); + await expect( + session.resolveRuntimeRequest?.({ + requestId: "ask-input", + turnId, + resolution: resolution as unknown as HarnessRuntimeRequestResolution, + }), + ).rejects.toThrow(/user_input rejected its resolution/); // A rejected resolution must leave the request answerable. expect(session.pendingRuntimeRequests?.()).toHaveLength(1); } await session.resolveRuntimeRequest?.({ requestId: "ask-input", turnId, - resolution: { action: "submit", answers: { field: { answers: ["staging"] } } }, + resolution: { + action: "submit", + answers: { field: { answers: ["staging"] } }, + }, + }); + expect(await userInput).toEqual({ + answers: { field: { answers: ["staging"] } }, }); - expect(await userInput).toEqual({ answers: { field: { answers: ["staging"] } } }); - const elicitation = open("ask-elicitation", "mcpServer/elicitation/request"); + const elicitation = open( + "ask-elicitation", + "mcpServer/elicitation/request", + ); await settled(); for (const resolution of [ { action: "submit" }, @@ -928,11 +1464,13 @@ describe("Codex app-server Codex driver", () => { { action: "submit", answers: { field: { answers: ["wrong shape"] } } }, { action: "accept_for_session" }, ]) { - await expect(session.resolveRuntimeRequest?.({ - requestId: "ask-elicitation", - turnId, - resolution: resolution as unknown as HarnessRuntimeRequestResolution, - })).rejects.toThrow(/elicitation rejected its resolution/); + await expect( + session.resolveRuntimeRequest?.({ + requestId: "ask-elicitation", + turnId, + resolution: resolution as unknown as HarnessRuntimeRequestResolution, + }), + ).rejects.toThrow(/elicitation rejected its resolution/); expect(session.pendingRuntimeRequests?.()).toHaveLength(1); } await session.resolveRuntimeRequest?.({ @@ -946,16 +1484,21 @@ describe("Codex app-server Codex driver", () => { _meta: null, }); - const approval = open("ask-approval", "item/commandExecution/requestApproval"); + const approval = open( + "ask-approval", + "item/commandExecution/requestApproval", + ); await settled(); - await expect(session.resolveRuntimeRequest?.({ - requestId: "ask-approval", - turnId, - resolution: { - action: "submit", - answers: { field: { answers: ["nope"] } }, - } as unknown as HarnessRuntimeRequestResolution, - })).rejects.toThrow(/command_approval rejected its resolution/); + await expect( + session.resolveRuntimeRequest?.({ + requestId: "ask-approval", + turnId, + resolution: { + action: "submit", + answers: { field: { answers: ["nope"] } }, + } as unknown as HarnessRuntimeRequestResolution, + }), + ).rejects.toThrow(/command_approval rejected its resolution/); await session.resolveRuntimeRequest?.({ requestId: "ask-approval", turnId, @@ -976,16 +1519,28 @@ describe("Codex app-server Codex driver", () => { void (async () => { for await (const event of session.events()) events.push(event); })(); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Approve this." } }); + const { turnId } = await session.startTurn({ + message: { role: "user", text: "Approve this." }, + }); const resolved = transport.invoke({ id: "outcome-resolved", method: "item/commandExecution/requestApproval", - params: { threadId: "thread-1", turnId, itemId: "item-resolved", reason: "approve" }, + params: { + threadId: "thread-1", + turnId, + itemId: "item-resolved", + reason: "approve", + }, }); const cancelled = transport.invoke({ id: "outcome-cancelled", method: "item/commandExecution/requestApproval", - params: { threadId: "thread-1", turnId, itemId: "item-cancelled", reason: "approve" }, + params: { + threadId: "thread-1", + turnId, + itemId: "item-cancelled", + reason: "approve", + }, }); await new Promise((resolve) => setTimeout(resolve, 0)); await session.resolveRuntimeRequest?.({ @@ -998,14 +1553,19 @@ describe("Codex app-server Codex driver", () => { await cancelled; await new Promise((resolve) => setTimeout(resolve, 0)); - const terminal = events.filter(({ eventType }) => - eventType === "runtime_request.resolved" || eventType === "runtime_request.cancelled"); - expect(terminal.map(({ eventType, turnId, itemId, payload }) => ({ - eventType, - turnId, - itemId, - payload, - }))).toEqual([ + const terminal = events.filter( + ({ eventType }) => + eventType === "runtime_request.resolved" || + eventType === "runtime_request.cancelled", + ); + expect( + terminal.map(({ eventType, turnId, itemId, payload }) => ({ + eventType, + turnId, + itemId, + payload, + })), + ).toEqual([ { eventType: "runtime_request.resolved", turnId, @@ -1035,14 +1595,18 @@ describe("Codex app-server Codex driver", () => { }); it("redacts browser-visible request details and diagnostics from the fixture markers", async () => { - const fixture = await loadLiveConsoleConformanceFixture(liveConsoleFixturePath); + const fixture = await loadLiveConsoleConformanceFixture( + liveConsoleFixturePath, + ); const transport = new FakeCodexTransport(); const session = await makeDriver([transport]).openSession({ runId: "run-redaction", normalizedSessionId: "normalized-redaction", workingDirectory: "/workspace", }); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Request input." } }); + const { turnId } = await session.startTurn({ + message: { role: "user", text: "Request input." }, + }); const pending = transport.invoke({ id: "redacted-request", method: "item/tool/requestUserInput", @@ -1056,7 +1620,8 @@ describe("Codex app-server Codex driver", () => { }); await new Promise((resolve) => setTimeout(resolve, 0)); const serialized = JSON.stringify(session.pendingRuntimeRequests?.()); - for (const marker of fixture.redactionMarkers) expect(serialized).not.toContain(marker); + for (const marker of fixture.redactionMarkers) + expect(serialized).not.toContain(marker); expect(serialized).toContain("[REDACTED]"); await session.resolveRuntimeRequest?.({ requestId: "redacted-request", @@ -1069,41 +1634,94 @@ describe("Codex app-server Codex driver", () => { it("makes duplicate semantic completion idempotent and rejects changed payloads", async () => { const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ runId: "run-result", normalizedSessionId: "normalized-result", workingDirectory: "/workspace" }); + const session = await makeDriver([transport]).openSession({ + runId: "run-result", + normalizedSessionId: "normalized-result", + workingDirectory: "/workspace", + }); await session.startTurn({ message: { role: "user", text: "Complete." } }); - const request = { id: 1, method: "item/tool/call", params: { threadId: "thread-1", turnId: "turn-1", callId: "call-1", tool: "paperclip_finish", arguments: result } }; + const request = { + id: 1, + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "call-1", + tool: "paperclip_finish", + arguments: result, + }, + }; expect(await transport.invoke(request)).toMatchObject({ success: true }); - expect(await transport.invoke({ - ...request, - id: 2, - params: { ...request.params, callId: "call-2", arguments: reverseObjectKeys(result) }, - })).toMatchObject({ success: true }); + expect( + await transport.invoke({ + ...request, + id: 2, + params: { + ...request.params, + callId: "call-2", + arguments: reverseObjectKeys(result), + }, + }), + ).toMatchObject({ success: true }); expect((await session.snapshot()).semanticResult?.callId).toBe("call-1"); const changed = structuredClone(result); changed.summary = "Changed after commit."; - expect(await transport.invoke({ ...request, id: 3, params: { ...request.params, arguments: changed } })).toMatchObject({ success: false }); - transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } }); - transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } }); + expect( + await transport.invoke({ + ...request, + id: 3, + params: { ...request.params, arguments: changed }, + }), + ).toMatchObject({ success: false }); + transport.push("turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", status: "completed", items: [] }, + }); + transport.push("turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", status: "completed", items: [] }, + }); const events = await collectUntilTerminal(session.events()); - expect(events.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(1); - expect(events.filter((event) => event.eventType === "turn.completed")).toHaveLength(1); + expect( + events.filter((event) => event.eventType === "run.result.proposed"), + ).toHaveLength(1); + expect( + events.filter((event) => event.eventType === "turn.completed"), + ).toHaveLength(1); }); it("advertises and dispatches run-authorized control-plane tools", async () => { const transport = new FakeCodexTransport(); - const handler = vi.fn(async (call) => ({ task: { id: "issue-1" }, callId: call.callId })); + const handler = vi.fn(async (call) => ({ + task: { id: "issue-1" }, + callId: call.callId, + })); const session = await makeDriver([transport], { - dynamicTools: [{ - name: "get_task_context", - description: "Read the assigned task.", - inputSchema: { type: "object", additionalProperties: false }, - }], + dynamicTools: [ + { + name: "get_task_context", + description: "Read the assigned task.", + inputSchema: { type: "object", additionalProperties: false }, + }, + ], dynamicToolHandler: handler, - }).openSession({ runId: "run-tools", normalizedSessionId: "normalized-tools", workingDirectory: "/workspace" }); - await session.startTurn({ message: { role: "user", text: "Inspect the task." } }); + }).openSession({ + runId: "run-tools", + normalizedSessionId: "normalized-tools", + workingDirectory: "/workspace", + }); + await session.startTurn({ + message: { role: "user", text: "Inspect the task." }, + }); - expect(transport.calls.find((call) => call.method === "thread/start")?.params.dynamicTools) - .toEqual(expect.arrayContaining([expect.objectContaining({ name: "get_task_context" })])); + expect( + transport.calls.find((call) => call.method === "thread/start")?.params + .dynamicTools, + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "get_task_context" }), + ]), + ); const response = await transport.invoke({ id: "rpc-tool", method: "item/tool/call", @@ -1116,13 +1734,20 @@ describe("Codex app-server Codex driver", () => { }, }); expect(response).toMatchObject({ success: true }); - expect(JSON.parse(String((response.contentItems as Array>)[0]?.text))) - .toEqual({ task: { id: "issue-1" }, callId: "call-context" }); - expect(handler).toHaveBeenCalledWith(expect.objectContaining({ - tool: "get_task_context", - callId: "call-context", - arguments: {}, - })); + expect( + JSON.parse( + String( + (response.contentItems as Array>)[0]?.text, + ), + ), + ).toEqual({ task: { id: "issue-1" }, callId: "call-context" }); + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ + tool: "get_task_context", + callId: "call-context", + arguments: {}, + }), + ); }); it("fails closed when an agent message changes a tool-committed result", async () => { @@ -1133,30 +1758,42 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect(await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: result, - }, - })).toMatchObject({ success: true }); + expect( + await transport.invoke({ + id: "tool-result", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "tool-result", + tool: "paperclip_finish", + arguments: result, + }, + }), + ).toMatchObject({ success: true }); const changed = structuredClone(result); changed.summary = "Conflicting agent-message result."; transport.push("item/completed", { threadId: "thread-1", turnId: "turn-1", - item: { id: "message-result", type: "agentMessage", text: JSON.stringify(changed) }, + item: { + id: "message-result", + type: "agentMessage", + text: JSON.stringify(changed), + }, }); const events: PrpEvent[] = []; for await (const event of session.events()) events.push(event); - expect(events.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(1); - expect(events.find((event) => event.eventType === "session.failed")?.payload) - .toMatchObject({ code: "conflicting_semantic_result", recoverable: false }); + expect( + events.filter((event) => event.eventType === "run.result.proposed"), + ).toHaveLength(1); + expect( + events.find((event) => event.eventType === "session.failed")?.payload, + ).toMatchObject({ + code: "conflicting_semantic_result", + recoverable: false, + }); expect((await session.snapshot()).semanticResult?.result).toEqual(result); }); @@ -1171,30 +1808,40 @@ describe("Codex app-server Codex driver", () => { transport.push("item/completed", { threadId: "thread-1", turnId: "turn-1", - item: { id: "message-result", type: "agentMessage", text: JSON.stringify(result) }, + item: { + id: "message-result", + type: "agentMessage", + text: JSON.stringify(result), + }, }); await new Promise((resolve) => setTimeout(resolve, 0)); const changed = structuredClone(result); changed.summary = "Conflicting tool result."; - expect(await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: changed, - }, - })).toMatchObject({ success: false }); + expect( + await transport.invoke({ + id: "tool-result", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "tool-result", + tool: "paperclip_finish", + arguments: changed, + }, + }), + ).toMatchObject({ success: false }); transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] }, }); const events = await collectUntilTerminal(session.events()); - expect(events.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(1); - expect(events.some((event) => event.eventType === "session.failed")).toBe(false); + expect( + events.filter((event) => event.eventType === "run.result.proposed"), + ).toHaveLength(1); + expect(events.some((event) => event.eventType === "session.failed")).toBe( + false, + ); expect((await session.snapshot()).semanticResult).toMatchObject({ callId: "message-result", result, @@ -1209,17 +1856,19 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect(await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: result, - }, - })).toMatchObject({ success: true }); + expect( + await transport.invoke({ + id: "tool-result", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "tool-result", + tool: "paperclip_finish", + arguments: result, + }, + }), + ).toMatchObject({ success: true }); transport.push("item/completed", { threadId: "thread-1", turnId: "turn-1", @@ -1235,8 +1884,12 @@ describe("Codex app-server Codex driver", () => { }); const events = await collectUntilTerminal(session.events()); - expect(events.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(1); - expect((await session.snapshot()).semanticResult?.callId).toBe("tool-result"); + expect( + events.filter((event) => event.eventType === "run.result.proposed"), + ).toHaveLength(1); + expect((await session.snapshot()).semanticResult?.callId).toBe( + "tool-result", + ); }); it("fails closed when a live terminal embeds a changed semantic result", async () => { @@ -1247,17 +1900,19 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect(await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: result, - }, - })).toMatchObject({ success: true }); + expect( + await transport.invoke({ + id: "tool-result", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "tool-result", + tool: "paperclip_finish", + arguments: result, + }, + }), + ).toMatchObject({ success: true }); const changed = structuredClone(result); changed.summary = "Conflicting terminal result."; transport.push("turn/completed", { @@ -1265,40 +1920,68 @@ describe("Codex app-server Codex driver", () => { turn: { id: "turn-1", status: "completed", - items: [{ id: "terminal-result", type: "agentMessage", text: JSON.stringify(changed) }], + items: [ + { + id: "terminal-result", + type: "agentMessage", + text: JSON.stringify(changed), + }, + ], }, }); const events: PrpEvent[] = []; for await (const event of session.events()) events.push(event); - expect(events.find((event) => event.eventType === "session.failed")?.payload) - .toMatchObject({ code: "conflicting_semantic_result", recoverable: false }); - expect(events.some((event) => event.eventType === "turn.completed")).toBe(false); + expect( + events.find((event) => event.eventType === "session.failed")?.payload, + ).toMatchObject({ + code: "conflicting_semantic_result", + recoverable: false, + }); + expect(events.some((event) => event.eventType === "turn.completed")).toBe( + false, + ); expect((await session.snapshot()).semanticResult?.result).toEqual(result); }); it.each([ { threadId: "other-thread", turnId: "turn-1", label: "another thread" }, { threadId: "thread-1", turnId: "other-turn", label: "another turn" }, - ])("rejects a semantic result for $label without committing it", async ({ threadId, turnId }) => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-hostile-tool", - normalizedSessionId: "normalized-hostile-tool", - workingDirectory: "/workspace", - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect(await transport.invoke({ - id: "hostile-call", - method: "item/tool/call", - params: { threadId, turnId, callId: "hostile-call", tool: "paperclip_finish", arguments: result }, - })).toMatchObject({ success: false }); - const events = await collectUntilTerminal(session.events()); - expect(events.some((event) => event.eventType === "run.result.proposed")).toBe(false); - expect(events.find((event) => event.eventType === "session.failed")?.payload) - .toMatchObject({ code: "tool_binding_mismatch", recoverable: false }); - expect(events.find((event) => event.eventType === "turn.failed")?.turnId).toBe("turn-1"); - }); + ])( + "rejects a semantic result for $label without committing it", + async ({ threadId, turnId }) => { + const transport = new FakeCodexTransport(); + const session = await makeDriver([transport]).openSession({ + runId: "run-hostile-tool", + normalizedSessionId: "normalized-hostile-tool", + workingDirectory: "/workspace", + }); + await session.startTurn({ message: { role: "user", text: "Complete." } }); + expect( + await transport.invoke({ + id: "hostile-call", + method: "item/tool/call", + params: { + threadId, + turnId, + callId: "hostile-call", + tool: "paperclip_finish", + arguments: result, + }, + }), + ).toMatchObject({ success: false }); + const events = await collectUntilTerminal(session.events()); + expect( + events.some((event) => event.eventType === "run.result.proposed"), + ).toBe(false); + expect( + events.find((event) => event.eventType === "session.failed")?.payload, + ).toMatchObject({ code: "tool_binding_mismatch", recoverable: false }); + expect( + events.find((event) => event.eventType === "turn.failed")?.turnId, + ).toBe("turn-1"); + }, + ); it("rejects pre-turn, cross-thread, and post-terminal notifications", async () => { const preTurnTransport = new FakeCodexTransport(); @@ -1310,13 +1993,21 @@ describe("Codex app-server Codex driver", () => { preTurnTransport.push("item/completed", { threadId: "thread-1", turnId: "turn-1", - item: { id: "answer-pre", type: "agentMessage", text: JSON.stringify(result) }, + item: { + id: "answer-pre", + type: "agentMessage", + text: JSON.stringify(result), + }, }); const preTurnEvents: PrpEvent[] = []; for await (const event of preTurn.events()) preTurnEvents.push(event); - expect(preTurnEvents.some((event) => event.eventType === "run.result.proposed")).toBe(false); - expect(preTurnEvents.find((event) => event.eventType === "session.failed")?.payload) - .toMatchObject({ code: "turn_binding_mismatch" }); + expect( + preTurnEvents.some((event) => event.eventType === "run.result.proposed"), + ).toBe(false); + expect( + preTurnEvents.find((event) => event.eventType === "session.failed") + ?.payload, + ).toMatchObject({ code: "turn_binding_mismatch" }); const crossThreadTransport = new FakeCodexTransport(); const crossThread = await makeDriver([crossThreadTransport]).openSession({ @@ -1324,16 +2015,28 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "normalized-cross-thread", workingDirectory: "/workspace", }); - await crossThread.startTurn({ message: { role: "user", text: "Complete." } }); + await crossThread.startTurn({ + message: { role: "user", text: "Complete." }, + }); crossThreadTransport.push("item/completed", { threadId: "other-thread", turnId: "turn-1", - item: { id: "answer-other", type: "agentMessage", text: JSON.stringify(result) }, + item: { + id: "answer-other", + type: "agentMessage", + text: JSON.stringify(result), + }, }); const crossThreadEvents = await collectUntilTerminal(crossThread.events()); - expect(crossThreadEvents.some((event) => event.eventType === "run.result.proposed")).toBe(false); - expect(crossThreadEvents.find((event) => event.eventType === "session.failed")?.payload) - .toMatchObject({ code: "thread_binding_mismatch" }); + expect( + crossThreadEvents.some( + (event) => event.eventType === "run.result.proposed", + ), + ).toBe(false); + expect( + crossThreadEvents.find((event) => event.eventType === "session.failed") + ?.payload, + ).toMatchObject({ code: "thread_binding_mismatch" }); const postTerminalTransport = new FakeCodexTransport(); const postTerminal = await makeDriver([postTerminalTransport]).openSession({ @@ -1341,23 +2044,27 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "normalized-post-terminal", workingDirectory: "/workspace", }); - await postTerminal.startTurn({ message: { role: "user", text: "Complete." } }); + await postTerminal.startTurn({ + message: { role: "user", text: "Complete." }, + }); postTerminalTransport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] }, }); await collectUntilTerminal(postTerminal.events()); - expect(await postTerminalTransport.invoke({ - id: "late-call", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "late-call", - tool: "paperclip_finish", - arguments: result, - }, - })).toMatchObject({ success: false }); + expect( + await postTerminalTransport.invoke({ + id: "late-call", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "late-call", + tool: "paperclip_finish", + arguments: result, + }, + }), + ).toMatchObject({ success: false }); }); it("rejects duplicate and conflicting terminal facts", async () => { @@ -1374,14 +2081,24 @@ describe("Codex app-server Codex driver", () => { }); transport.push("turn/completed", { threadId: "thread-1", - turn: { id: "turn-1", status: "failed", error: { message: "changed" }, items: [] }, + turn: { + id: "turn-1", + status: "failed", + error: { message: "changed" }, + items: [], + }, }); const events: PrpEvent[] = []; for await (const event of session.events()) events.push(event); - expect(events.filter((event) => event.eventType === "turn.completed")).toHaveLength(1); - expect(events.filter((event) => event.eventType === "turn.failed")).toHaveLength(0); - expect(events.find((event) => event.eventType === "session.failed")?.payload) - .toMatchObject({ code: "conflicting_turn_terminal", recoverable: false }); + expect( + events.filter((event) => event.eventType === "turn.completed"), + ).toHaveLength(1); + expect( + events.filter((event) => event.eventType === "turn.failed"), + ).toHaveLength(0); + expect( + events.find((event) => event.eventType === "session.failed")?.payload, + ).toMatchObject({ code: "conflicting_turn_terminal", recoverable: false }); }); it("rejects oversized semantic results without retaining provider payloads", async () => { @@ -1392,33 +2109,50 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect(await transport.invoke({ - id: "large-call", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "large-call", - tool: "paperclip_finish", - arguments: { ...result, summary: "x".repeat(70 * 1024) }, - }, - })).toEqual({ + expect( + await transport.invoke({ + id: "large-call", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "large-call", + tool: "paperclip_finish", + arguments: { ...result, summary: "x".repeat(70 * 1024) }, + }, + }), + ).toEqual({ success: false, - contentItems: [{ type: "inputText", text: "Semantic result exceeded the retained payload limit." }], + contentItems: [ + { + type: "inputText", + text: "Semantic result exceeded the retained payload limit.", + }, + ], }); transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] }, }); const events = await collectUntilTerminal(session.events()); - expect(events.some((event) => JSON.stringify(event).includes("x".repeat(1024)))).toBe(false); - expect(events.some((event) => event.eventType === "run.result.proposed")).toBe(false); + expect( + events.some((event) => JSON.stringify(event).includes("x".repeat(1024))), + ).toBe(false); + expect( + events.some((event) => event.eventType === "run.result.proposed"), + ).toBe(false); }); it("normalizes finish and block tools into one canonical result contract", async () => { const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ runId: "run-tools", normalizedSessionId: "normalized-tools", workingDirectory: "/workspace" }); - await session.startTurn({ message: { role: "user", text: "Complete or block." } }); + const session = await makeDriver([transport]).openSession({ + runId: "run-tools", + normalizedSessionId: "normalized-tools", + workingDirectory: "/workspace", + }); + await session.startTurn({ + message: { role: "user", text: "Complete or block." }, + }); const blocked: PrpStructuredRunResult = { ...structuredClone(result), reportedWorkDisposition: "blocked", @@ -1426,8 +2160,15 @@ describe("Codex app-server Codex driver", () => { completionClaim: { ...structuredClone(result.completionClaim), objectiveSatisfied: false, - criteria: [{ criterionId: "file", status: "not_satisfied", evidenceRefs: [] }], - remainingWork: [{ description: "Fixture owner must provide input.", blocksCompletion: true }], + criteria: [ + { criterionId: "file", status: "not_satisfied", evidenceRefs: [] }, + ], + remainingWork: [ + { + description: "Fixture owner must provide input.", + blocksCompletion: true, + }, + ], }, blocker: { reasonCode: "fixture_input_missing", @@ -1448,34 +2189,55 @@ describe("Codex app-server Codex driver", () => { arguments: blocked, }, }; - expect(await transport.invoke({ - ...request, - id: 9, - params: { ...request.params, tool: "paperclip_finish" }, - })).toMatchObject({ success: false }); + expect( + await transport.invoke({ + ...request, + id: 9, + params: { ...request.params, tool: "paperclip_finish" }, + }), + ).toMatchObject({ success: false }); expect(await transport.invoke(request)).toMatchObject({ success: true }); - expect(await transport.invoke({ ...request, id: 11 })).toMatchObject({ success: true }); - transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } }); + expect(await transport.invoke({ ...request, id: 11 })).toMatchObject({ + success: true, + }); + transport.push("turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", status: "completed", items: [] }, + }); const events = await collectUntilTerminal(session.events()); - expect(events.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(1); - expect(events.find((event) => event.eventType === "run.result.proposed")?.payload) - .toMatchObject({ reportedWorkDisposition: "blocked" }); + expect( + events.filter((event) => event.eventType === "run.result.proposed"), + ).toHaveLength(1); + expect( + events.find((event) => event.eventType === "run.result.proposed") + ?.payload, + ).toMatchObject({ reportedWorkDisposition: "blocked" }); }); it("resumes and reconciles the exact provider thread after transport loss", async () => { const first = new FakeCodexTransport(); const second = new FakeCodexTransport(); const driver = makeDriver([first, second]); - const original = await driver.openSession({ runId: "run-recover", normalizedSessionId: "normalized-recover", workingDirectory: "/workspace" }); + const original = await driver.openSession({ + runId: "run-recover", + normalizedSessionId: "normalized-recover", + workingDirectory: "/workspace", + }); await original.startTurn({ message: { role: "user", text: "Work." } }); const snapshot = await original.snapshot(); await original.close({ reason: "transport lost" }); const recovery = await driver.recoverSession?.(snapshot); expect(recovery).toMatchObject({ recovered: true }); expect(recovery?.session?.ids()).toEqual(original.ids()); - expect(await recovery?.session?.reconcile?.()).toMatchObject({ thread: { id: "thread-1" } }); + expect(await recovery?.session?.reconcile?.()).toMatchObject({ + thread: { id: "thread-1" }, + }); expect(second.calls.map((call) => call.method)).toEqual([ - "initialize", "thread/read", "thread/resume", "thread/goal/get", "thread/read", + "initialize", + "thread/read", + "thread/resume", + "thread/goal/get", + "thread/read", ]); expect((await recovery?.session?.snapshot())?.activeTurnId).toBe("turn-1"); }); @@ -1500,7 +2262,9 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "normalized-history", workingDirectory: "/workspace", }); - await original.startTurn({ message: { role: "user", text: "Keep working." } }); + await original.startTurn({ + message: { role: "user", text: "Keep working." }, + }); const snapshot = await original.snapshot(); await original.close({ reason: "transport lost" }); @@ -1508,11 +2272,15 @@ describe("Codex app-server Codex driver", () => { const recovered = recovery?.session; expect(recovered).toBeDefined(); await recovered!.reconcile?.(); - expect(await recovered!.snapshot()).toMatchObject({ activeTurnId: "turn-1" }); + expect(await recovered!.snapshot()).toMatchObject({ + activeTurnId: "turn-1", + }); await recovered!.close({ reason: "test complete" }); const events: PrpEvent[] = []; for await (const event of recovered!.events()) events.push(event); - expect(events.some((event) => event.eventType === "turn.completed")).toBe(false); + expect(events.some((event) => event.eventType === "turn.completed")).toBe( + false, + ); }); it("preserves proposal idempotency and rejects a changed result after transport loss", async () => { @@ -1525,17 +2293,19 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); await original.startTurn({ message: { role: "user", text: "Complete." } }); - expect(await first.invoke({ - id: "call-before-loss", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-before-loss", - tool: "paperclip_finish", - arguments: result, - }, - })).toMatchObject({ success: true }); + expect( + await first.invoke({ + id: "call-before-loss", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "call-before-loss", + tool: "paperclip_finish", + arguments: result, + }, + }), + ).toMatchObject({ success: true }); const snapshot = await original.snapshot(); expect(snapshot.semanticResult).toMatchObject({ callId: "call-before-loss", @@ -1545,44 +2315,56 @@ describe("Codex app-server Codex driver", () => { await original.close({ reason: "transport lost after proposal" }); const originalEvents: PrpEvent[] = []; for await (const event of original.events()) originalEvents.push(event); - expect(originalEvents.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(1); + expect( + originalEvents.filter( + (event) => event.eventType === "run.result.proposed", + ), + ).toHaveLength(1); const recovery = await driver.recoverSession?.(snapshot); const recovered = recovery?.session; expect(recovered).toBeDefined(); const collecting = collectUntilTerminal(recovered!.events()); await recovered!.reconcile?.(); - expect(await second.invoke({ - id: "call-after-loss", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-after-loss", - tool: "paperclip_finish", - arguments: reverseObjectKeys(result), - }, - })).toMatchObject({ success: true }); + expect( + await second.invoke({ + id: "call-after-loss", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "call-after-loss", + tool: "paperclip_finish", + arguments: reverseObjectKeys(result), + }, + }), + ).toMatchObject({ success: true }); const changed = structuredClone(result); changed.summary = "Changed after transport loss."; - expect(await second.invoke({ - id: "changed-after-loss", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "changed-after-loss", - tool: "paperclip_finish", - arguments: changed, - }, - })).toMatchObject({ success: false }); + expect( + await second.invoke({ + id: "changed-after-loss", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "changed-after-loss", + tool: "paperclip_finish", + arguments: changed, + }, + }), + ).toMatchObject({ success: false }); second.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] }, }); const events = await collecting; - expect(events.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(0); - expect(events.filter((event) => event.eventType === "turn.completed")).toHaveLength(1); + expect( + events.filter((event) => event.eventType === "run.result.proposed"), + ).toHaveLength(0); + expect( + events.filter((event) => event.eventType === "turn.completed"), + ).toHaveLength(1); }); it("rejects changed terminal result content discovered during recovery", async () => { @@ -1595,11 +2377,19 @@ describe("Codex app-server Codex driver", () => { id: "thread-1", sessionId: "provider-session-1", cwd: "/workspace", - turns: [{ - id: "turn-1", - status: "completed", - items: [{ id: "terminal-result", type: "agentMessage", text: JSON.stringify(changed) }], - }], + turns: [ + { + id: "turn-1", + status: "completed", + items: [ + { + id: "terminal-result", + type: "agentMessage", + text: JSON.stringify(changed), + }, + ], + }, + ], }, }; const driver = makeDriver([first, second]); @@ -1609,17 +2399,19 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); await original.startTurn({ message: { role: "user", text: "Complete." } }); - expect(await first.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: result, - }, - })).toMatchObject({ success: true }); + expect( + await first.invoke({ + id: "tool-result", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "tool-result", + tool: "paperclip_finish", + arguments: result, + }, + }), + ).toMatchObject({ success: true }); first.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] }, @@ -1631,13 +2423,18 @@ describe("Codex app-server Codex driver", () => { const recovery = await driver.recoverSession?.(snapshot); expect(recovery).toMatchObject({ recovered: true }); - await expect(recovery!.session!.reconcile!()) - .rejects.toMatchObject({ - name: "HarnessReconciliationError", - recoverable: true, - message: expect.stringContaining("contains a conflicting semantic result"), - }); - expect((await recovery!.session!.snapshot()).semanticResult?.result).toEqual(result); + await expect( + recovery!.session!.reconcile!(), + ).rejects.toMatchObject({ + name: "HarnessReconciliationError", + recoverable: true, + message: expect.stringContaining( + "contains a conflicting semantic result", + ), + }); + expect( + (await recovery!.session!.snapshot()).semanticResult?.result, + ).toEqual(result); await recovery!.session!.close({ reason: "test complete" }); }); @@ -1659,17 +2456,19 @@ describe("Codex app-server Codex driver", () => { workingDirectory: "/workspace", }); await original.startTurn({ message: { role: "user", text: "Complete." } }); - expect(await first.invoke({ - id: "terminal-call", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "terminal-call", - tool: "paperclip_finish", - arguments: result, - }, - })).toMatchObject({ success: true }); + expect( + await first.invoke({ + id: "terminal-call", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "terminal-call", + tool: "paperclip_finish", + arguments: result, + }, + }), + ).toMatchObject({ success: true }); first.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] }, @@ -1686,8 +2485,12 @@ describe("Codex app-server Codex driver", () => { await recovered!.close({ reason: "test complete" }); const events: PrpEvent[] = []; for await (const event of recovered!.events()) events.push(event); - expect(events.some((event) => event.eventType.startsWith("turn."))).toBe(false); - expect(events.some((event) => event.eventType === "run.result.proposed")).toBe(false); + expect(events.some((event) => event.eventType.startsWith("turn."))).toBe( + false, + ); + expect( + events.some((event) => event.eventType === "run.result.proposed"), + ).toBe(false); }); it("fails recovery explicitly when the persisted active turn is missing or replaced", async () => { @@ -1724,30 +2527,45 @@ describe("Codex app-server Codex driver", () => { await original.close({ reason: "transport lost" }); const recovery = await driver.recoverSession?.(snapshot); expect(recovery?.session).toBeDefined(); - await expect(recovery!.session!.reconcile!()) - .rejects.toMatchObject({ - name: "HarnessReconciliationError", - recoverable: true, - message: expect.stringContaining(testCase.message), - }); + await expect( + recovery!.session!.reconcile!(), + ).rejects.toMatchObject({ + name: "HarnessReconciliationError", + recoverable: true, + message: expect.stringContaining(testCase.message), + }); await recovery!.session!.close({ reason: "test complete" }); } }); it("reconciles a turn completed during transport loss without replacing either session identity", async () => { - const first = new FakeCodexTransport("driver-thread-loss", "provider-session-loss"); - const second = new FakeCodexTransport("driver-thread-loss", "provider-session-loss"); + const first = new FakeCodexTransport( + "driver-thread-loss", + "provider-session-loss", + ); + const second = new FakeCodexTransport( + "driver-thread-loss", + "provider-session-loss", + ); second.readResponse = { thread: { id: "driver-thread-loss", sessionId: "provider-session-loss", cwd: "/workspace", tokenUsage: { total: { inputTokens: 21, outputTokens: 8 } }, - turns: [{ - id: "turn-1", - status: "completed", - items: [{ id: "answer-loss", type: "agentMessage", text: JSON.stringify(result) }], - }], + turns: [ + { + id: "turn-1", + status: "completed", + items: [ + { + id: "answer-loss", + type: "agentMessage", + text: JSON.stringify(result), + }, + ], + }, + ], }, }; const driver = makeDriver([first, second]); @@ -1756,7 +2574,9 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "controller-session-loss", workingDirectory: "/workspace", }); - await original.startTurn({ message: { role: "user", text: "Work through loss." } }); + await original.startTurn({ + message: { role: "user", text: "Work through loss." }, + }); const snapshot = await original.snapshot(); await original.close({ reason: "transport lost before terminal delivery" }); @@ -1779,10 +2599,21 @@ describe("Codex app-server Codex driver", () => { driverSessionId: "driver-thread-loss", providerSessionId: "provider-session-loss", }); - expect(events.filter((event) => event.eventType === "turn.completed")).toHaveLength(1); - expect(events.filter((event) => event.eventType === "run.result.proposed")).toHaveLength(1); - expect(events.every((event, index) => event.sourceSeq === snapshot.lastSourceSequence! + index + 1)).toBe(true); - expect(await recovered!.usage?.()).toEqual({ total: { inputTokens: 21, outputTokens: 8 } }); + expect( + events.filter((event) => event.eventType === "turn.completed"), + ).toHaveLength(1); + expect( + events.filter((event) => event.eventType === "run.result.proposed"), + ).toHaveLength(1); + expect( + events.every( + (event, index) => + event.sourceSeq === snapshot.lastSourceSequence! + index + 1, + ), + ).toBe(true); + expect(await recovered!.usage?.()).toEqual({ + total: { inputTokens: 21, outputTokens: 8 }, + }); }); it("degrades unsupported operations with explicit redacted diagnostics", async () => { @@ -1791,17 +2622,30 @@ describe("Codex app-server Codex driver", () => { "turn/steer", new Error("method not found Bearer super-secret api_key=also-secret"), ); - const session = await makeDriver([transport]).openSession({ runId: "run-degrade", normalizedSessionId: "normalized-degrade", workingDirectory: "/workspace" }); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Start." } }); - await expect(session.steer?.({ turnId, message: { role: "user", text: "Steer." } })).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); + const session = await makeDriver([transport]).openSession({ + runId: "run-degrade", + normalizedSessionId: "normalized-degrade", + workingDirectory: "/workspace", + }); + const { turnId } = await session.startTurn({ + message: { role: "user", text: "Start." }, + }); + await expect( + session.steer?.({ turnId, message: { role: "user", text: "Steer." } }), + ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); const iterator = session.events()[Symbol.asyncIterator](); const events: PrpEvent[] = []; for (let index = 0; index < 6; index += 1) { const next = await iterator.next(); if (next.value) events.push(next.value); } - const diagnostic = events.find((event) => event.eventType === "harness.diagnostic"); - expect(diagnostic?.payload).toMatchObject({ code: "unsupported_operation", operation: "steering" }); + const diagnostic = events.find( + (event) => event.eventType === "harness.diagnostic", + ); + expect(diagnostic?.payload).toMatchObject({ + code: "unsupported_operation", + operation: "steering", + }); expect(JSON.stringify(diagnostic)).not.toContain("super-secret"); expect(JSON.stringify(diagnostic)).not.toContain("also-secret"); }); @@ -1815,12 +2659,17 @@ describe("Codex app-server Codex driver", () => { }); const unknownCriterion = structuredClone(result); - unknownCriterion.completionClaim.criteria = [{ - criterionId: "not-in-envelope", - status: "satisfied", - evidenceRefs: [], - }]; - const criterionDecision = validateCodexResultProposal(unknownCriterion, envelope); + unknownCriterion.completionClaim.criteria = [ + { + criterionId: "not-in-envelope", + status: "satisfied", + evidenceRefs: [], + }, + ]; + const criterionDecision = validateCodexResultProposal( + unknownCriterion, + envelope, + ); expect(criterionDecision).toMatchObject({ status: "rejected" }); if (criterionDecision.status === "rejected") { expect(criterionDecision.issues.map((issue) => issue.code)).toEqual( @@ -1839,38 +2688,54 @@ describe("Codex app-server Codex driver", () => { it.each(["needs_review", "blocked"] as const)( "keeps a completed runtime successful for an accepted %s advisory proposal", async (disposition) => { - const proposal: PrpStructuredRunResult = disposition === "needs_review" - ? { - ...structuredClone(result), - reportedWorkDisposition: "needs_review", - attentionRequests: [{ kind: "review", summary: "Review the completed artifact." }], - } - : { - ...structuredClone(result), - reportedWorkDisposition: "blocked", - summary: "Waiting on fixture input.", - completionClaim: { - ...structuredClone(result.completionClaim), - objectiveSatisfied: false, - criteria: [{ criterionId: "file", status: "not_satisfied", evidenceRefs: [] }], - remainingWork: [{ description: "Provide fixture input.", blocksCompletion: true }], - }, - blocker: { - reasonCode: "fixture_input_missing", - owner: { kind: "external", name: "fixture owner" }, - unblockAction: "Provide fixture input.", - scope: "task_wide", - }, - artifacts: [], - }; + const proposal: PrpStructuredRunResult = + disposition === "needs_review" + ? { + ...structuredClone(result), + reportedWorkDisposition: "needs_review", + attentionRequests: [ + { kind: "review", summary: "Review the completed artifact." }, + ], + } + : { + ...structuredClone(result), + reportedWorkDisposition: "blocked", + summary: "Waiting on fixture input.", + completionClaim: { + ...structuredClone(result.completionClaim), + objectiveSatisfied: false, + criteria: [ + { + criterionId: "file", + status: "not_satisfied", + evidenceRefs: [], + }, + ], + remainingWork: [ + { + description: "Provide fixture input.", + blocksCompletion: true, + }, + ], + }, + blocker: { + reasonCode: "fixture_input_missing", + owner: { kind: "external", name: "fixture owner" }, + unblockAction: "Provide fixture input.", + scope: "task_wide", + }, + artifacts: [], + }; const { trace } = await traceCompletedProposal(proposal); expect(trace.resultDecision.status).toBe("accepted"); - expect(trace.events.find((event) => event.eventType === "run.terminal")?.payload) - .toMatchObject({ - turnTerminalState: "completed", - runTerminalState: "succeeded", - reportedWorkDisposition: disposition, - }); + expect( + trace.events.find((event) => event.eventType === "run.terminal") + ?.payload, + ).toMatchObject({ + turnTerminalState: "completed", + runTerminalState: "succeeded", + reportedWorkDisposition: disposition, + }); }, ); @@ -1879,18 +2744,31 @@ describe("Codex app-server Codex driver", () => { wrongRevision.completionClaim.contractRevision = "wrong-revision"; const rejected = await traceCompletedProposal(wrongRevision); expect(rejected.trace.result).toBeNull(); - expect(rejected.trace.proposedResult).toMatchObject({ reportedWorkDisposition: "done" }); - expect(rejected.trace.events.find((event) => event.eventType === "run.result.rejected")?.payload) - .toMatchObject({ recovery: { required: true, recoverable: true } }); - expect(rejected.trace.events.find((event) => event.eventType === "run.terminal")?.payload) - .toMatchObject({ runTerminalState: "succeeded", reportedWorkDisposition: "yielded" }); + expect(rejected.trace.proposedResult).toMatchObject({ + reportedWorkDisposition: "done", + }); + expect( + rejected.trace.events.find( + (event) => event.eventType === "run.result.rejected", + )?.payload, + ).toMatchObject({ recovery: { required: true, recoverable: true } }); + expect( + rejected.trace.events.find((event) => event.eventType === "run.terminal") + ?.payload, + ).toMatchObject({ + runTerminalState: "succeeded", + reportedWorkDisposition: "yielded", + }); const missing = await traceCompletedProposal(null); expect(missing.trace.resultDecision).toMatchObject({ status: "rejected" }); - expect(missing.trace.events.some((event) => - event.eventType === "run.result.proposed" && - event.payload.reportedWorkDisposition === "needs_review" - )).toBe(false); + expect( + missing.trace.events.some( + (event) => + event.eventType === "run.result.proposed" && + event.payload.reportedWorkDisposition === "needs_review", + ), + ).toBe(false); }); it("preserves four independent stable identities under controller ownership", async () => { @@ -1905,10 +2783,13 @@ describe("Codex app-server Codex driver", () => { driverSessionId: "thread-1", providerSessionId: "provider-session-1", }); - expect(trace.events.every((event) => - event.runId === "runtime-run-identity" && - event.normalizedSessionId === "controller-session-identity" - )).toBe(true); + expect( + trace.events.every( + (event) => + event.runId === "runtime-run-identity" && + event.normalizedSessionId === "controller-session-identity", + ), + ).toBe(true); }); it("validates persisted identity, uniqueness, terminals, and line bounds before replay", async () => { @@ -1916,29 +2797,48 @@ describe("Codex app-server Codex driver", () => { runId: "runtime-run-persisted", normalizedSessionId: "controller-session-persisted", }); - const serialize = (events: PrpEvent[]) => `${events.map((event) => JSON.stringify(event)).join("\n")}\n`; - expect(replayPersistedCodexEvents(serialize(trace.events), trace.metadata)) - .toEqual(trace.replaySnapshot); + const serialize = (events: PrpEvent[]) => + `${events.map((event) => JSON.stringify(event)).join("\n")}\n`; + expect( + replayPersistedCodexEvents(serialize(trace.events), trace.metadata), + ).toEqual(trace.replaySnapshot); const mismatched = structuredClone(trace.events); mismatched[0]!.normalizedSessionId = "another-session"; - expect(() => replayPersistedCodexEvents(serialize(mismatched), trace.metadata)) - .toThrow("identity did not match"); + expect(() => + replayPersistedCodexEvents(serialize(mismatched), trace.metadata), + ).toThrow("identity did not match"); - const terminalIndex = trace.events.findIndex((event) => event.eventType === "run.terminal"); - const duplicated = trace.events.toSpliced(terminalIndex, 0, structuredClone(trace.events[0]!)); - expect(() => replayPersistedCodexEvents(serialize(duplicated), trace.metadata)) - .toThrow("event id was duplicated"); + const terminalIndex = trace.events.findIndex( + (event) => event.eventType === "run.terminal", + ); + const duplicated = trace.events.toSpliced( + terminalIndex, + 0, + structuredClone(trace.events[0]!), + ); + expect(() => + replayPersistedCodexEvents(serialize(duplicated), trace.metadata), + ).toThrow("event id was duplicated"); const terminal = structuredClone(trace.events[terminalIndex]!); terminal.sourceEventId = `${terminal.sourceEventId}:conflict`; terminal.sourceSeq += 1; - expect(() => replayPersistedCodexEvents(serialize([...trace.events, terminal]), trace.metadata)) - .toThrow("after the run terminal"); - expect(() => replayPersistedCodexEvents(`{\"payload\":\"${"x".repeat(300 * 1024)}\"}\n`, trace.metadata)) - .toThrow("line was empty or oversized"); - expect(() => replayPersistedCodexEvents("{not-json}\n", trace.metadata)) - .toThrow("malformed JSON"); + expect(() => + replayPersistedCodexEvents( + serialize([...trace.events, terminal]), + trace.metadata, + ), + ).toThrow("after the run terminal"); + expect(() => + replayPersistedCodexEvents( + `{\"payload\":\"${"x".repeat(300 * 1024)}\"}\n`, + trace.metadata, + ), + ).toThrow("line was empty or oversized"); + expect(() => + replayPersistedCodexEvents("{not-json}\n", trace.metadata), + ).toThrow("malformed JSON"); }); it("degrades declared unsupported capabilities without Codex-specific core branches", async () => { @@ -1958,15 +2858,19 @@ describe("Codex app-server Codex driver", () => { }); expect(trace.resultDecision.status).toBe("accepted"); expect(trace.assertions.contextIsSkillless).toBe(true); - expect(trace.diagnostics).toEqual(expect.arrayContaining([ - expect.stringContaining("steering is unavailable"), - expect.stringContaining("interruption is unavailable"), - ])); + expect(trace.diagnostics).toEqual( + expect.arrayContaining([ + expect.stringContaining("steering is unavailable"), + expect.stringContaining("interruption is unavailable"), + ]), + ); expect(transport.calls.map((call) => call.method)).not.toEqual( expect.arrayContaining(["turn/steer", "turn/interrupt", "thread/read"]), ); - expect(transport.calls.find((call) => call.method === "thread/start")?.params.dynamicTools) - .toEqual([]); + expect( + transport.calls.find((call) => call.method === "thread/start")?.params + .dynamicTools, + ).toEqual([]); const directTransport = new FakeCodexTransport(); const driver = makeDriver([directTransport], { capabilities }); @@ -1975,8 +2879,12 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "normalized-no-capabilities", workingDirectory: "/workspace", }); - await expect(session.read?.()).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); - await expect(session.usage?.()).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); + await expect(session.read?.()).rejects.toBeInstanceOf( + HarnessCapabilityUnavailableError, + ); + await expect(session.usage?.()).rejects.toBeInstanceOf( + HarnessCapabilityUnavailableError, + ); const snapshot = await session.snapshot(); expect(await driver.recoverSession?.(snapshot)).toMatchObject({ recovered: false, diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.ts index d38fd2c2c26..dc72809daf2 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.ts @@ -55,6 +55,7 @@ import { type CodexAppServerTransport, type CodexRpcNotification, type CodexRpcServerRequest, + type CodexTraceInterpretation, type CodexTransportProcessInfo, } from "./app-server-transport.js"; @@ -80,7 +81,8 @@ class AsyncQueue implements AsyncIterable { close(): void { if (this.#closed) return; this.#closed = true; - for (const waiter of this.#waiters.splice(0)) waiter({ value: undefined, done: true }); + for (const waiter of this.#waiters.splice(0)) + waiter({ value: undefined, done: true }); } [Symbol.asyncIterator](): AsyncIterator { @@ -99,6 +101,12 @@ export interface CodexAppServerDriverOptions { taskEnvelope: CodexTaskEnvelope; conversationMode?: "task" | "direct"; requestedCollaborationMode?: "default" | "plan"; + /** + * Include Codex's built-in collaboration instructions. Defaults to true so + * interactive runs receive native commentary/preambles. Deterministic evals + * may opt out explicitly without changing the production default. + */ + includeCollaborationModeInstructions?: boolean; transportFactory?: () => CodexAppServerTransport; /** Additional control-plane tools exposed to the provider for this run. */ dynamicTools?: readonly Readonly>[]; @@ -167,14 +175,20 @@ function text(value: unknown, fallback = ""): string { return typeof value === "string" ? value : fallback; } -function boundedText(value: unknown, fallback = "unknown", maxCharacters = 1024): string { +function boundedText( + value: unknown, + fallback = "unknown", + maxCharacters = 1024, +): string { const candidate = text(value, fallback); return candidate.length <= maxCharacters ? candidate : `${candidate.slice(0, maxCharacters)}...[truncated]`; } -function itemFromParams(params: Record): Record { +function itemFromParams( + params: Record, +): Record { return record(params.item); } @@ -189,7 +203,9 @@ function userInput(message: NativeUserMessage): Record { return { type: "text", text: message.text, text_elements: [] }; } -function terminalState(status: string): "completed" | "failed" | "interrupted" | "cancelled" { +function terminalState( + status: string, +): "completed" | "failed" | "interrupted" | "cancelled" { if (status === "failed") return "failed"; if (status === "interrupted") return "interrupted"; if (status === "cancelled") return "cancelled"; @@ -212,7 +228,10 @@ function tryParseResult(value: unknown): PrpStructuredRunResult | null { function canonicalJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; const object = record(value); - if (Object.keys(object).length > 0 || (typeof value === "object" && value !== null)) { + if ( + Object.keys(object).length > 0 || + (typeof value === "object" && value !== null) + ) { return `{${Object.keys(object) .sort() .map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`) @@ -232,29 +251,34 @@ function finishToolSpec(): Record { function blockToolSpec(): Record { return { name: CODEX_BLOCK_TOOL_NAME, - description: "Return the one semantic result when the task cannot continue.", + description: + "Return the one semantic result when the task cannot continue.", inputSchema: CODEX_BLOCK_RESULT_OUTPUT_SCHEMA, }; } -export function codexSemanticToolSpecs(): readonly Readonly>[] { +export function codexSemanticToolSpecs(): readonly Readonly< + Record +>[] { return [finishToolSpec(), blockToolSpec()]; } function dynamicToolResponse(value: unknown): Record { return { success: true, - contentItems: [{ - type: "inputText", - text: typeof value === "string" ? value : JSON.stringify(value), - }], + contentItems: [ + { + type: "inputText", + text: typeof value === "string" ? value : JSON.stringify(value), + }, + ], }; } const SKILLLESS_BASE_CONFIG = { "skills.include_instructions": false, include_apps_instructions: false, - include_collaboration_mode_instructions: false, + include_collaboration_mode_instructions: true, "features.apps": false, "features.plugins": false, "features.multi_agent": false, @@ -262,9 +286,18 @@ const SKILLLESS_BASE_CONFIG = { "features.image_generation": false, } as const; -function commandEnvironment(source: NodeJS.ProcessEnv = process.env): Record { +function commandEnvironment( + source: NodeJS.ProcessEnv = process.env, +): Record { const environment: Record = {}; - for (const key of ["PATH", "PATHEXT", "SystemRoot", "WINDIR", "LANG", "LC_ALL"] as const) { + for (const key of [ + "PATH", + "PATHEXT", + "SystemRoot", + "WINDIR", + "LANG", + "LC_ALL", + ] as const) { const value = source[key]; if (value !== undefined) environment[key] = value; } @@ -274,14 +307,23 @@ function commandEnvironment(source: NodeJS.ProcessEnv = process.env): Record { - return { ...SKILLLESS_BASE_CONFIG }; -} - -function collaborationThreadConfig(mode: "default" | "plan") { return { ...SKILLLESS_BASE_CONFIG, - include_collaboration_mode_instructions: mode === "plan", + include_collaboration_mode_instructions: + includeCollaborationModeInstructions, + }; +} + +function collaborationThreadConfig( + _mode: "default" | "plan", + includeCollaborationModeInstructions = true, +) { + return { + ...SKILLLESS_BASE_CONFIG, + include_collaboration_mode_instructions: + includeCollaborationModeInstructions, }; } @@ -292,9 +334,16 @@ function tomlString(value: string): string { export function createIsolatedCodexAppServerArgs( source: NodeJS.ProcessEnv = process.env, ): string[] { - const deniedHostRoots = [...new Set([source.HOME, source.CODEX_HOME] - .filter((value): value is string => typeof value === "string" && value.trim().length > 0) - .map((value) => resolve(value)))]; + const deniedHostRoots = [ + ...new Set( + [source.HOME, source.CODEX_HOME] + .filter( + (value): value is string => + typeof value === "string" && value.trim().length > 0, + ) + .map((value) => resolve(value)), + ), + ]; const filesystemRules = [ `\":root\"=\"none\"`, `\":minimal\"=\"read\"`, @@ -340,11 +389,18 @@ function securedThreadParams( workingDirectory: string, source: NodeJS.ProcessEnv = process.env, mode: "default" | "plan" = "default", + includeCollaborationModeInstructions = true, ): Record { - const permissionProfile = mode === "plan" ? PLANNING_PERMISSION_PROFILE : SKILLLESS_PERMISSION_PROFILE; + const permissionProfile = + mode === "plan" + ? PLANNING_PERMISSION_PROFILE + : SKILLLESS_PERMISSION_PROFILE; return { cwd: workingDirectory, - config: collaborationThreadConfig(mode), + config: collaborationThreadConfig( + mode, + includeCollaborationModeInstructions, + ), permissions: permissionProfile, runtimeWorkspaceRoots: [workingDirectory], }; @@ -421,20 +477,34 @@ export class CodexAppServerDriver implements HarnessDriver { } async openSession(input: OpenHarnessSessionInput): Promise { - const workingDirectory = validateWorkingDirectory(input.workingDirectory, this.#options.environment); + const workingDirectory = validateWorkingDirectory( + input.workingDirectory, + this.#options.environment, + ); const transport = this.#transport(); try { await this.#persistProcessOwnership(transport); const initialize = await this.#initialize(transport); - const requestedMode = this.#options.requestedCollaborationMode ?? "default"; + const requestedMode = + this.#options.requestedCollaborationMode ?? "default"; const response = await transport.request("thread/start", { - ...securedThreadParams(workingDirectory, this.#options.environment, requestedMode), + ...securedThreadParams( + workingDirectory, + this.#options.environment, + requestedMode, + this.#options.includeCollaborationModeInstructions ?? true, + ), approvalPolicy: "never", - ...(this.#direct() ? {} : { baseInstructions: CODEX_SKILLLESS_BASE_INSTRUCTIONS }), + ...(this.#direct() + ? {} + : { baseInstructions: CODEX_SKILLLESS_BASE_INSTRUCTIONS }), dynamicTools: this.#direct() ? [] : this.#caps.dynamicTools - ? [...(this.#options.dynamicTools ?? []), ...codexSemanticToolSpecs()] + ? [ + ...(this.#options.dynamicTools ?? []), + ...codexSemanticToolSpecs(), + ] : [], experimentalRawEvents: false, persistExtendedHistory: false, @@ -444,9 +514,15 @@ export class CodexAppServerDriver implements HarnessDriver { response, requestedMode, ); - const opened = this.#openedThread(response, initialize, workingDirectory, collaborationMode); + const opened = this.#openedThread( + response, + initialize, + workingDirectory, + collaborationMode, + ); const goal = await this.#discoverGoal(transport, opened.threadId); - if (opened.context.liveConsole) opened.context.liveConsole.goals = this.#caps.goals; + if (opened.context.liveConsole) + opened.context.liveConsole.goals = this.#caps.goals; return this.#session({ transport, runId: input.runId, @@ -469,10 +545,20 @@ export class CodexAppServerDriver implements HarnessDriver { return { recovered: false, reason: "resume capability is unavailable" }; } if (!this.#caps.read) { - return { recovered: false, reason: "read capability is required for safe resume" }; + return { + recovered: false, + reason: "read capability is required for safe resume", + }; } - if (!snapshot.runId || !snapshot.normalizedSessionId || !snapshot.driverSessionId) { - return { recovered: false, reason: "persisted session identity is incomplete" }; + if ( + !snapshot.runId || + !snapshot.normalizedSessionId || + !snapshot.driverSessionId + ) { + return { + recovered: false, + reason: "persisted session identity is incomplete", + }; } const transport = this.#transport(); try { @@ -485,7 +571,10 @@ export class CodexAppServerDriver implements HarnessDriver { const existingThread = record(existing.thread); if (text(existingThread.id) !== snapshot.driverSessionId) { await transport.close(); - return { recovered: false, reason: "provider read a different session" }; + return { + recovered: false, + reason: "provider read a different session", + }; } const workingDirectory = validateWorkingDirectory( text(existingThread.cwd), @@ -497,8 +586,11 @@ export class CodexAppServerDriver implements HarnessDriver { workingDirectory, this.#options.environment, this.#options.requestedCollaborationMode ?? "default", + this.#options.includeCollaborationModeInstructions ?? true, ), - baseInstructions: this.#direct() ? "" : CODEX_SKILLLESS_BASE_INSTRUCTIONS, + baseInstructions: this.#direct() + ? "" + : CODEX_SKILLLESS_BASE_INSTRUCTIONS, persistExtendedHistory: false, }); const collaborationMode = await this.#negotiateCollaborationMode( @@ -506,20 +598,32 @@ export class CodexAppServerDriver implements HarnessDriver { response, this.#options.requestedCollaborationMode ?? "default", ); - const opened = this.#openedThread(response, initialize, workingDirectory, collaborationMode); + const opened = this.#openedThread( + response, + initialize, + workingDirectory, + collaborationMode, + ); if (opened.threadId !== snapshot.driverSessionId) { await transport.close(); - return { recovered: false, reason: "provider resumed a different session" }; + return { + recovered: false, + reason: "provider resumed a different session", + }; } if ( snapshot.providerSessionId && opened.providerSessionId !== snapshot.providerSessionId ) { await transport.close(); - return { recovered: false, reason: "provider resumed a different provider session" }; + return { + recovered: false, + reason: "provider resumed a different provider session", + }; } const goal = await this.#discoverGoal(transport, opened.threadId); - if (opened.context.liveConsole) opened.context.liveConsole.goals = this.#caps.goals; + if (opened.context.liveConsole) + opened.context.liveConsole.goals = this.#caps.goals; return { recovered: true, session: this.#session({ @@ -564,14 +668,17 @@ export class CodexAppServerDriver implements HarnessDriver { try { const response = await transport.request("collaborationMode/list", {}); const preset = Array.isArray(response.data) - ? response.data.map(record).find((candidate) => text(candidate.mode) === "plan") + ? response.data + .map(record) + .find((candidate) => text(candidate.mode) === "plan") : undefined; if (!preset) throw new Error("plan preset is absent"); const model = text( preset.model, text(threadResponse.model, text(record(threadResponse.thread).model)), ); - if (model.length === 0) throw new Error("plan preset did not resolve a model"); + if (model.length === 0) + throw new Error("plan preset did not resolve a model"); return { mode: "plan", settings: { @@ -589,9 +696,12 @@ export class CodexAppServerDriver implements HarnessDriver { } } - async #persistProcessOwnership(transport: CodexAppServerTransport): Promise { + async #persistProcessOwnership( + transport: CodexAppServerTransport, + ): Promise { if (!this.#options.onSpawn) return; - const processInfo: CodexTransportProcessInfo | undefined = transport.processInfo?.(); + const processInfo: CodexTransportProcessInfo | undefined = + transport.processInfo?.(); if (!processInfo || processInfo.exited || processInfo.pid === null) return; await this.#options.onSpawn({ pid: processInfo.pid, @@ -600,9 +710,15 @@ export class CodexAppServerDriver implements HarnessDriver { }); } - async #initialize(transport: CodexAppServerTransport): Promise> { + async #initialize( + transport: CodexAppServerTransport, + ): Promise> { const initialized = await transport.request("initialize", { - clientInfo: { name: "paperclip-runner", title: "Paperclip Runner", version: DRIVER_VERSION }, + clientInfo: { + name: "paperclip-runner", + title: "Paperclip Runner", + version: DRIVER_VERSION, + }, capabilities: { experimentalApi: true, requestAttestation: false }, }); transport.notify("initialized"); @@ -645,18 +761,22 @@ export class CodexAppServerDriver implements HarnessDriver { ): OpenedCodexThread { const thread = record(response.thread); const threadId = text(thread.id); - if (threadId.length === 0) throw new Error("Codex thread response omitted thread.id"); + if (threadId.length === 0) + throw new Error("Codex thread response omitted thread.id"); const activePermissionProfile = record(thread.activePermissionProfile); const permissionProfileId = text(activePermissionProfile.id); const requestedMode = this.#options.requestedCollaborationMode ?? "default"; - const requiredPermissionProfile = requestedMode === "plan" - ? PLANNING_PERMISSION_PROFILE - : SKILLLESS_PERMISSION_PROFILE; + const requiredPermissionProfile = + requestedMode === "plan" + ? PLANNING_PERMISSION_PROFILE + : SKILLLESS_PERMISSION_PROFILE; if ( permissionProfileId.length > 0 && permissionProfileId !== requiredPermissionProfile ) { - throw new Error("Codex thread did not activate the required filesystem permission profile"); + throw new Error( + "Codex thread did not activate the required filesystem permission profile", + ); } const configuredPermissionProfile = { ...activePermissionProfile, @@ -664,7 +784,9 @@ export class CodexAppServerDriver implements HarnessDriver { }; const returnedWorkingDirectory = text(response.cwd, workingDirectory); if (resolve(returnedWorkingDirectory) !== workingDirectory) { - throw new Error("Codex thread response changed the assigned working directory"); + throw new Error( + "Codex thread response changed the assigned working directory", + ); } return { threadId, @@ -679,7 +801,10 @@ export class CodexAppServerDriver implements HarnessDriver { version: DRIVER_VERSION, }, model: boundedText(response.model), - modelProvider: boundedText(response.modelProvider, boundedText(thread.modelProvider)), + modelProvider: boundedText( + response.modelProvider, + boundedText(thread.modelProvider), + ), workingDirectory, collaborationMode: collaborationMode === null ? "default" : "plan", sandbox: { @@ -701,7 +826,8 @@ export class CodexAppServerDriver implements HarnessDriver { instructionPolicy: { skillInstructions: false, appInstructions: false, - collaborationInstructions: requestedMode === "plan", + collaborationInstructions: + this.#options.includeCollaborationModeInstructions ?? true, }, environmentKeys: Object.keys( commandEnvironment(this.#options.environment), @@ -709,7 +835,12 @@ export class CodexAppServerDriver implements HarnessDriver { dynamicToolNames: this.#direct() ? [] : this.#caps.dynamicTools - ? [...(this.#options.dynamicTools ?? []).map((tool) => text(tool.name)), ...CODEX_SEMANTIC_TOOL_NAMES] + ? [ + ...(this.#options.dynamicTools ?? []).map((tool) => + text(tool.name), + ), + ...CODEX_SEMANTIC_TOOL_NAMES, + ] : [], modelInputKinds: ["text"], liveConsole: { @@ -780,7 +911,10 @@ class CodexHarnessSession implements HarnessSession { readonly #terminalTurns = new Map(); readonly #workspaceChangesByTurn = new Map>(); readonly #emittedFileReferences = new Set(); - readonly #itemChannels = new Map(); + readonly #itemChannels = new Map< + string, + "progress" | "final" | "summary" | "detail" | "unknown" + >(); readonly #pendingRuntimeRequests = new Map(); readonly #lineage = new Map(); #goal: HarnessThreadGoal | null = null; @@ -829,12 +963,22 @@ class CodexHarnessSession implements HarnessSession { this.#lineage.set(entry.threadId, structuredClone(entry)); } if (!this.#lineage.has(input.opened.lineage.threadId)) { - this.#lineage.set(input.opened.lineage.threadId, structuredClone(input.opened.lineage)); + this.#lineage.set( + input.opened.lineage.threadId, + structuredClone(input.opened.lineage), + ); } if (input.semanticResult) { - const validation = validatePrpStructuredRunResult(input.semanticResult.result); - if (!validation.ok || canonicalJson(validation.result) !== input.semanticResult.fingerprint) { - throw new HarnessReconciliationError("persisted semantic result fingerprint is invalid"); + const validation = validatePrpStructuredRunResult( + input.semanticResult.result, + ); + if ( + !validation.ok || + canonicalJson(validation.result) !== input.semanticResult.fingerprint + ) { + throw new HarnessReconciliationError( + "persisted semantic result fingerprint is invalid", + ); } this.#result = structuredClone(validation.result); this.#resultFingerprint = input.semanticResult.fingerprint; @@ -842,13 +986,22 @@ class CodexHarnessSession implements HarnessSession { this.#resultTurnId = input.semanticResult.turnId; } for (const terminal of input.terminalTurns ?? []) { - if (!terminal.turnId || !terminal.fingerprint || this.#terminalTurns.has(terminal.turnId)) { - throw new HarnessReconciliationError("persisted terminal turn fingerprints are invalid"); + if ( + !terminal.turnId || + !terminal.fingerprint || + this.#terminalTurns.has(terminal.turnId) + ) { + throw new HarnessReconciliationError( + "persisted terminal turn fingerprints are invalid", + ); } this.#terminalTurns.set(terminal.turnId, terminal.fingerprint); } - this.#terminal = this.#conversationMode === "task" && this.#terminalTurns.size > 0; - this.#transport.setServerRequestHandler((request) => this.#handleServerRequest(request)); + this.#terminal = + this.#conversationMode === "task" && this.#terminalTurns.size > 0; + this.#transport.setServerRequestHandler((request) => + this.#handleServerRequest(request), + ); this.#emit(input.resumed ? "session.resumed" : "session.started", { driverSessionId: input.opened.threadId, providerSessionId: input.opened.providerSessionId, @@ -861,20 +1014,28 @@ class CodexHarnessSession implements HarnessSession { { turnId: stale.turnId, itemId: stale.itemId }, ); } - this.#emit("item.completed", { - kind: "thread_lineage", - text: `Root thread ${input.opened.threadId}`, - lineage: input.opened.lineage, - }, { itemId: `thread:${input.opened.threadId}` }); - this.#emit("item.completed", { - kind: "model", - text: `${input.opened.context.model} (${input.opened.context.modelProvider})`, - model: { - name: input.opened.context.model, - provider: input.opened.context.modelProvider, - codexVersion: input.opened.context.codexVersion, + this.#emit( + "item.completed", + { + kind: "thread_lineage", + text: `Root thread ${input.opened.threadId}`, + lineage: input.opened.lineage, }, - }, { itemId: `${input.opened.threadId}:model` }); + { itemId: `thread:${input.opened.threadId}` }, + ); + this.#emit( + "item.completed", + { + kind: "model", + text: `${input.opened.context.model} (${input.opened.context.modelProvider})`, + model: { + name: input.opened.context.model, + provider: input.opened.context.modelProvider, + codexVersion: input.opened.context.codexVersion, + }, + }, + { itemId: `${input.opened.threadId}:model` }, + ); void this.#pumpNotifications(); } @@ -887,7 +1048,11 @@ class CodexHarnessSession implements HarnessSession { } async attachRun(input: { runId: string }): Promise { - if (this.#activeTurnId !== null || this.#turnStartPending || this.#pendingRuntimeRequests.size > 0) { + if ( + this.#activeTurnId !== null || + this.#turnStartPending || + this.#pendingRuntimeRequests.size > 0 + ) { throw new Error("codex_run_attach_busy"); } if (!input.runId) throw new Error("codex_run_attach_invalid"); @@ -921,8 +1086,16 @@ class CodexHarnessSession implements HarnessSession { async startTurn(input: { message: NativeUserMessage; requestedCollaborationMode?: "default" | "plan"; - }): Promise<{ turnId: string; effectiveCollaborationMode: "default" | "plan" }> { - if (this.#terminal || this.#protocolFailed || this.#activeTurnId !== null || this.#turnStartPending) { + }): Promise<{ + turnId: string; + effectiveCollaborationMode: "default" | "plan"; + }> { + if ( + this.#terminal || + this.#protocolFailed || + this.#activeTurnId !== null || + this.#turnStartPending + ) { throw this.#unsupported( "turn start", this.#protocolFailureCode === null @@ -930,11 +1103,18 @@ class CodexHarnessSession implements HarnessSession { : `session failed protocol validation: ${this.#protocolFailureCode} (${this.#protocolFailureMessage ?? "no detail"})`, ); } - const taskText = this.#conversationMode === "direct" - ? input.message.text - : JSON.stringify({ task: this.#taskEnvelope, message: input.message.text }); + const taskText = + this.#conversationMode === "direct" + ? input.message.text + : JSON.stringify({ + task: this.#taskEnvelope, + message: input.message.text, + }); const effectiveCollaborationMode = this.#opened.context.collaborationMode; - if (input.requestedCollaborationMode && input.requestedCollaborationMode !== effectiveCollaborationMode) { + if ( + input.requestedCollaborationMode && + input.requestedCollaborationMode !== effectiveCollaborationMode + ) { throw new Error( `collaboration_mode_mismatch: requested ${input.requestedCollaborationMode}, effective ${effectiveCollaborationMode}`, ); @@ -945,7 +1125,8 @@ class CodexHarnessSession implements HarnessSession { this.#emit("turn.submitted", { envelopeSchema: this.#taskEnvelope.schema, text: input.message.text, - requestedCollaborationMode: input.requestedCollaborationMode ?? effectiveCollaborationMode, + requestedCollaborationMode: + input.requestedCollaborationMode ?? effectiveCollaborationMode, effectiveCollaborationMode, }); this.#turnStartPending = true; @@ -955,24 +1136,31 @@ class CodexHarnessSession implements HarnessSession { response = await this.#transport.request("turn/start", { threadId: this.#opened.threadId, cwd: this.#opened.context.workingDirectory, - permissions: requestedMode === "plan" - ? PLANNING_PERMISSION_PROFILE - : SKILLLESS_PERMISSION_PROFILE, + permissions: + requestedMode === "plan" + ? PLANNING_PERMISSION_PROFILE + : SKILLLESS_PERMISSION_PROFILE, runtimeWorkspaceRoots: [this.#opened.context.workingDirectory], ...(this.#opened.collaborationMode === null ? {} : { collaborationMode: this.#opened.collaborationMode }), input: [userInput({ role: "user", text: taskText })], - ...(this.#conversationMode === "direct" ? {} : { outputSchema: CODEX_RESULT_OUTPUT_SCHEMA }), + ...(this.#conversationMode === "direct" + ? {} + : { outputSchema: CODEX_RESULT_OUTPUT_SCHEMA }), }); } finally { this.#turnStartPending = false; } const turn = record(response.turn); const turnId = text(turn.id); - if (turnId.length === 0) throw new Error("Codex turn response omitted turn.id"); + if (turnId.length === 0) + throw new Error("Codex turn response omitted turn.id"); if (this.#activeTurnId !== null && this.#activeTurnId !== turnId) { - this.#failProtocol("turn_start_mismatch", "turn/start response disagreed with turn/started"); + this.#failProtocol( + "turn_start_mismatch", + "turn/start response disagreed with turn/started", + ); throw new Error("Codex turn identity changed during start"); } this.#activeTurnId ??= turnId; @@ -987,13 +1175,20 @@ class CodexHarnessSession implements HarnessSession { }; } - async steer(input: { turnId: string; message: NativeUserMessage; correlationId?: string }): Promise { + async steer(input: { + turnId: string; + message: NativeUserMessage; + correlationId?: string; + }): Promise { this.#requireCapability("steering"); this.#requireActiveTurn(input.turnId, "steering"); if (input.correlationId) { - const acknowledgedTurnId = this.#acknowledgedSteeringCorrelations.get(input.correlationId); + const acknowledgedTurnId = this.#acknowledgedSteeringCorrelations.get( + input.correlationId, + ); if (acknowledgedTurnId) { - if (acknowledgedTurnId !== input.turnId) throw new HarnessOperationAlreadyTerminalError("steering"); + if (acknowledgedTurnId !== input.turnId) + throw new HarnessOperationAlreadyTerminalError("steering"); return; } } @@ -1008,18 +1203,25 @@ class CodexHarnessSession implements HarnessSession { throw new HarnessOperationAlreadyTerminalError("steering"); } if (input.correlationId) { - this.#acknowledgedSteeringCorrelations.set(input.correlationId, input.turnId); + this.#acknowledgedSteeringCorrelations.set( + input.correlationId, + input.turnId, + ); } - this.#emit("item.completed", { - kind: "steering_acknowledgement", - text: "Steering acknowledged for the active turn.", - status: "acknowledged", - }, { - turnId: input.turnId, - itemId: input.correlationId - ? `${input.turnId}:steer:${input.correlationId}` - : `${input.turnId}:steer:${++this.#steerSequence}`, - }); + this.#emit( + "item.completed", + { + kind: "steering_acknowledgement", + text: "Steering acknowledged for the active turn.", + status: "acknowledged", + }, + { + turnId: input.turnId, + itemId: input.correlationId + ? `${input.turnId}:steer:${input.correlationId}` + : `${input.turnId}:steer:${++this.#steerSequence}`, + }, + ); } catch (error) { if (error instanceof HarnessOperationAlreadyTerminalError) throw error; const detail = redactCodexDiagnostic(String(error)); @@ -1037,11 +1239,15 @@ class CodexHarnessSession implements HarnessSession { this.#requireCapability("interruption"); if (this.#turnStartPending && this.#activeTurnId === null) { this.#interruptQueued = true; - this.#emit("item.completed", { - kind: "interrupt_acknowledgement", - text: "Interrupt queued until the provider assigns the turn identity.", - status: "queued", - }, { itemId: `interrupt:queued:${++this.#interruptSequence}` }); + this.#emit( + "item.completed", + { + kind: "interrupt_acknowledgement", + text: "Interrupt queued until the provider assigns the turn identity.", + status: "queued", + }, + { itemId: `interrupt:queued:${++this.#interruptSequence}` }, + ); return; } if (this.#terminal || this.#activeTurnId === null) { @@ -1061,15 +1267,19 @@ class CodexHarnessSession implements HarnessSession { if (this.#activeTurnId !== turnId) { throw new HarnessOperationAlreadyTerminalError("interruption"); } - this.#emit("item.completed", { - kind: "interrupt_acknowledgement", - text: "Interrupt accepted for the active turn.", - status: "acknowledged", - reason: boundedText(reason), - }, { - turnId, - itemId: `${turnId}:interrupt:${++this.#interruptSequence}`, - }); + this.#emit( + "item.completed", + { + kind: "interrupt_acknowledgement", + text: "Interrupt accepted for the active turn.", + status: "acknowledged", + reason: boundedText(reason), + }, + { + turnId, + itemId: `${turnId}:interrupt:${++this.#interruptSequence}`, + }, + ); } catch (error) { if (error instanceof HarnessOperationAlreadyTerminalError) throw error; throw this.#unsupported("interruption", error); @@ -1078,7 +1288,8 @@ class CodexHarnessSession implements HarnessSession { pendingRuntimeRequests(): HarnessRuntimeRequest[] { return [...this.#pendingRuntimeRequests.values()].map(({ request }) => - structuredClone(request)); + structuredClone(request), + ); } async resolveRuntimeRequest(input: { @@ -1094,7 +1305,10 @@ class CodexHarnessSession implements HarnessSession { `request ${input.requestId} is no longer pending`, ); } - if (pending.request.turnId !== input.turnId || this.#activeTurnId !== input.turnId) { + if ( + pending.request.turnId !== input.turnId || + this.#activeTurnId !== input.turnId + ) { throw new HarnessStaleTurnError(input.turnId); } const resolution = parseHarnessRuntimeRequestResolution( @@ -1105,7 +1319,9 @@ class CodexHarnessSession implements HarnessSession { this.#pendingRuntimeRequests.delete(input.requestId); this.#emit( "runtime_request.resolved", - harnessRuntimeRequestOutcome(pending.request, { action: resolution.action }), + harnessRuntimeRequestOutcome(pending.request, { + action: resolution.action, + }), { turnId: input.turnId, itemId: pending.request.itemId }, ); pending.settle(response); @@ -1126,28 +1342,41 @@ class CodexHarnessSession implements HarnessSession { ...params, objective: input.objective, status: "active", - ...(input.tokenBudget !== undefined ? { tokenBudget: input.tokenBudget } : {}), + ...(input.tokenBudget !== undefined + ? { tokenBudget: input.tokenBudget } + : {}), }; } else { - params = { ...params, status: input.action === "pause" ? "paused" : "active" }; + params = { + ...params, + status: input.action === "pause" ? "paused" : "active", + }; } } try { const response = await this.#transport.request(method, params); - const goal = input.action === "clear" ? null : parseThreadGoal(response.goal); + const goal = + input.action === "clear" ? null : parseThreadGoal(response.goal); if (!["get", "clear"].includes(input.action) && goal === null) { throw new Error(`${method} omitted its goal`); } this.#goal = goal; const kind = input.action === "clear" ? "goal_cleared" : "goal"; - this.#emit("item.completed", { - kind, - text: goal === null - ? input.action === "clear" ? "Thread goal cleared." : "No thread goal is configured." - : `Thread goal ${goal.status}: ${goal.objective}`, - action: input.action, - goal, - }, { itemId: `${this.#opened.threadId}:goal:${this.#sourceSequence + 1}` }); + this.#emit( + "item.completed", + { + kind, + text: + goal === null + ? input.action === "clear" + ? "Thread goal cleared." + : "No thread goal is configured." + : `Thread goal ${goal.status}: ${goal.objective}`, + action: input.action, + goal, + }, + { itemId: `${this.#opened.threadId}:goal:${this.#sourceSequence + 1}` }, + ); return goal === null ? null : structuredClone(goal); } catch (error) { throw this.#unsupported(`goal ${input.action}`, error); @@ -1175,21 +1404,31 @@ class CodexHarnessSession implements HarnessSession { const snapshot = await this.read(); const thread = record(snapshot.thread); if (text(thread.id) !== this.#opened.threadId) { - throw new HarnessReconciliationError("thread/read returned a different driver session"); + throw new HarnessReconciliationError( + "thread/read returned a different driver session", + ); } const providerSessionId = text(thread.sessionId); if ( this.#opened.providerSessionId !== null && providerSessionId !== this.#opened.providerSessionId ) { - throw new HarnessReconciliationError("thread/read returned a different provider session"); + throw new HarnessReconciliationError( + "thread/read returned a different provider session", + ); } const turns = Array.isArray(thread.turns) ? thread.turns.map(record) : []; - const reconciledUsage = boundedPayload(record(thread.tokenUsage ?? snapshot.tokenUsage)); + const reconciledUsage = boundedPayload( + record(thread.tokenUsage ?? snapshot.tokenUsage), + ); if (Object.keys(reconciledUsage).length > 0) this.#usage = reconciledUsage; - const activeTurns = turns.filter((turn) => text(turn.status) === "inProgress"); + const activeTurns = turns.filter( + (turn) => text(turn.status) === "inProgress", + ); const expectedTurnId = this.#activeTurnId; - const unexpectedActive = activeTurns.find((turn) => text(turn.id) !== expectedTurnId); + const unexpectedActive = activeTurns.find( + (turn) => text(turn.id) !== expectedTurnId, + ); if (unexpectedActive !== undefined) { throw new HarnessReconciliationError( `thread/read exposed active turn ${text(unexpectedActive.id)} instead of persisted active turn ${expectedTurnId ?? "none"}`, @@ -1198,7 +1437,9 @@ class CodexHarnessSession implements HarnessSession { let reconciledTerminalTurnId: string | null = null; if (expectedTurnId !== null) { - const expectedTurn = turns.find((turn) => text(turn.id) === expectedTurnId); + const expectedTurn = turns.find( + (turn) => text(turn.id) === expectedTurnId, + ); if (expectedTurn === undefined) { throw new HarnessReconciliationError( `persisted active turn ${expectedTurnId} is missing from thread/read`, @@ -1207,7 +1448,9 @@ class CodexHarnessSession implements HarnessSession { const status = text(expectedTurn.status); if (status === "inProgress") { this.#activeTurnId = expectedTurnId; - } else if (["completed", "failed", "interrupted", "cancelled"].includes(status)) { + } else if ( + ["completed", "failed", "interrupted", "cancelled"].includes(status) + ) { reconciledTerminalTurnId = expectedTurnId; this.#mapTerminalTurn(expectedTurn, expectedTurnId, true); } else { @@ -1220,7 +1463,11 @@ class CodexHarnessSession implements HarnessSession { for (const [turnId, fingerprint] of this.#terminalTurns) { const observed = turns.find((turn) => text(turn.id) === turnId); if (observed === undefined) continue; - if (!["completed", "failed", "interrupted", "cancelled"].includes(text(observed.status))) { + if ( + !["completed", "failed", "interrupted", "cancelled"].includes( + text(observed.status), + ) + ) { throw new HarnessReconciliationError( `previously terminal turn ${turnId} is no longer terminal in thread/read`, ); @@ -1254,14 +1501,17 @@ class CodexHarnessSession implements HarnessSession { runId: this.#runId, normalizedSessionId: this.#normalizedSessionId, activeTurnId: this.#activeTurnId, - semanticResult: this.#result === null || this.#resultFingerprint === null || this.#resultTurnId === null - ? null - : { - result: structuredClone(this.#result), - fingerprint: this.#resultFingerprint, - callId: this.#resultCallId, - turnId: this.#resultTurnId, - }, + semanticResult: + this.#result === null || + this.#resultFingerprint === null || + this.#resultTurnId === null + ? null + : { + result: structuredClone(this.#result), + fingerprint: this.#resultFingerprint, + callId: this.#resultCallId, + turnId: this.#resultTurnId, + }, terminalTurns: [...this.#terminalTurns].map(([turnId, fingerprint]) => ({ turnId, fingerprint, @@ -1297,6 +1547,52 @@ class CodexHarnessSession implements HarnessSession { } #mapNotification(notification: CodexRpcNotification): void { + const sourceSequenceBefore = this.#sourceSequence; + let rejected = false; + try { + this.#mapNotificationBody(notification); + } catch (error) { + rejected = true; + throw error; + } finally { + const correlation = notification.paperclipTrace; + if (correlation !== undefined) { + const emittedEventIds: string[] = []; + for ( + let sourceSeq = sourceSequenceBefore + 1; + sourceSeq <= this.#sourceSequence; + sourceSeq += 1 + ) { + emittedEventIds.push( + `${this.#runnerInstanceId}:${this.#runId}:${sourceSeq}`, + ); + } + const disposition: CodexTraceInterpretation["disposition"] = rejected + ? "rejected" + : emittedEventIds.length > 0 + ? "mapped" + : "ignored"; + try { + this.#transport.recordTraceInterpretation?.({ + sourceEventId: correlation.sourceEventId, + sourceEventType: correlation.sourceEventType, + providerMethod: notification.method, + disposition, + emittedEventIds, + reason: rejected + ? "Codex driver rejected the rehydrated provider notification" + : emittedEventIds.length > 0 + ? "Codex driver normalized the rehydrated provider notification into persisted canonical PRP events" + : "Codex driver accepted the rehydrated provider notification but emitted no canonical PRP event", + }); + } catch { + // Trace delivery is deliberately outside run authority. + } + } + } + } + + #mapNotificationBody(notification: CodexRpcNotification): void { if (!isBoundCodexNotification(notification.method)) return; const params = notification.params; const turn = record(params.turn); @@ -1306,19 +1602,30 @@ class CodexHarnessSession implements HarnessSession { const itemId = text(item.id, text(params.itemId)); if ( (threadId.length === 0 || threadId === this.#opened.threadId) && - (turnId.length === 0 || this.#activeTurnId === null || turnId === this.#activeTurnId) + (turnId.length === 0 || + this.#activeTurnId === null || + turnId === this.#activeTurnId) ) { - for (const canonical of canonicalProviderEventsFromCodex(notification.method, params)) { + for (const canonical of canonicalProviderEventsFromCodex( + notification.method, + params, + )) { this.#emit(canonical.eventType, canonical.payload, { turnId: turnId || undefined, itemId: canonical.itemId, }); } } - if (notification.method === "error" || notification.method === "warning" || notification.method === "configWarning") { + if ( + notification.method === "error" || + notification.method === "warning" || + notification.method === "configWarning" + ) { this.#emit("harness.diagnostic", { code: notification.method.replaceAll("/", "_"), - message: redactCodexDiagnostic(text(params.message, JSON.stringify(boundedCodexValue(params)))), + message: redactCodexDiagnostic( + text(params.message, JSON.stringify(boundedCodexValue(params))), + ), }); return; } @@ -1335,33 +1642,45 @@ class CodexHarnessSession implements HarnessSession { return; } this.#lineage.set(lineage.threadId, lineage); - this.#emit("item.started", { - kind: "thread_lineage", - text: `${lineage.nickname ?? lineage.role ?? "Child agent"} started.`, - lineage, - }, { itemId: `thread:${lineage.threadId}` }); + this.#emit( + "item.started", + { + kind: "thread_lineage", + text: `${lineage.nickname ?? lineage.role ?? "Child agent"} started.`, + lineage, + }, + { itemId: `thread:${lineage.threadId}` }, + ); return; } if (notification.method === "thread/status/changed") { const lineage = this.#lineage.get(threadId); if (lineage === undefined || threadId === this.#opened.threadId) return; lineage.status = threadStatus(params.status); - this.#emit("item.delta", { - kind: "thread_lineage", - text: `${lineage.nickname ?? lineage.role ?? "Child agent"}: ${lineage.status}`, - lineage, - }, { itemId: `thread:${lineage.threadId}` }); + this.#emit( + "item.delta", + { + kind: "thread_lineage", + text: `${lineage.nickname ?? lineage.role ?? "Child agent"}: ${lineage.status}`, + lineage, + }, + { itemId: `thread:${lineage.threadId}` }, + ); return; } if (notification.method === "thread/closed") { const lineage = this.#lineage.get(threadId); if (lineage === undefined || threadId === this.#opened.threadId) return; lineage.status = "closed"; - this.#emit("item.completed", { - kind: "thread_lineage", - text: `${lineage.nickname ?? lineage.role ?? "Child agent"} closed.`, - lineage, - }, { itemId: `thread:${lineage.threadId}` }); + this.#emit( + "item.completed", + { + kind: "thread_lineage", + text: `${lineage.nickname ?? lineage.role ?? "Child agent"} closed.`, + lineage, + }, + { itemId: `thread:${lineage.threadId}` }, + ); return; } if (notification.method === "thread/goal/updated") { @@ -1369,23 +1688,34 @@ class CodexHarnessSession implements HarnessSession { const goal = parseThreadGoal(params.goal); if (goal === null) return; this.#goal = goal; - this.#emit("item.completed", { - kind: "goal", - text: `Thread goal ${goal.status}: ${goal.objective}`, - action: "notification", - goal, - }, { turnId: turnId || undefined, itemId: `${threadId}:goal:update:${this.#sourceSequence + 1}` }); + this.#emit( + "item.completed", + { + kind: "goal", + text: `Thread goal ${goal.status}: ${goal.objective}`, + action: "notification", + goal, + }, + { + turnId: turnId || undefined, + itemId: `${threadId}:goal:update:${this.#sourceSequence + 1}`, + }, + ); return; } if (notification.method === "thread/goal/cleared") { if (threadId !== this.#opened.threadId) return; this.#goal = null; - this.#emit("item.completed", { - kind: "goal_cleared", - text: "Thread goal cleared.", - action: "notification", - goal: null, - }, { itemId: `${threadId}:goal:clear:${this.#sourceSequence + 1}` }); + this.#emit( + "item.completed", + { + kind: "goal_cleared", + text: "Thread goal cleared.", + action: "notification", + goal: null, + }, + { itemId: `${threadId}:goal:clear:${this.#sourceSequence + 1}` }, + ); return; } if (notification.method === "serverRequest/resolved") { @@ -1395,7 +1725,9 @@ class CodexHarnessSession implements HarnessSession { this.#pendingRuntimeRequests.delete(requestId); this.#emit( "runtime_request.cancelled", - harnessRuntimeRequestOutcome(pending.request, { reason: "provider_resolved" }), + harnessRuntimeRequestOutcome(pending.request, { + reason: "provider_resolved", + }), { turnId: pending.request.turnId, itemId: pending.request.itemId }, ); pending.settle(safeRequestResponse(pending.request.method, "cancel")); @@ -1417,12 +1749,19 @@ class CodexHarnessSession implements HarnessSession { (!this.#turnStartPending && this.#activeTurnId === null) || (this.#activeTurnId !== null && this.#activeTurnId !== turnId) ) { - this.#failProtocol("turn_binding_mismatch", "Provider started an unexpected turn."); + this.#failProtocol( + "turn_binding_mismatch", + "Provider started an unexpected turn.", + ); return; } this.#activeTurnId = turnId; this.#turnStarted = true; - this.#emit("turn.started", { status: text(turn.status, "inProgress") }, { turnId }); + this.#emit( + "turn.started", + { status: text(turn.status, "inProgress") }, + { turnId }, + ); return; } if (notification.method === "turn/completed") { @@ -1438,28 +1777,36 @@ class CodexHarnessSession implements HarnessSession { if (!this.#notificationNamesActiveTurn(turnId, "item start")) return; const channel = this.#channelForStartedItem(item); if (itemId) this.#itemChannels.set(itemId, channel); - this.#emit("item.started", boundedPayload({ - kind: text(item.type, "unknown"), - channel, - providerPhase: text(item.phase) || undefined, - text: itemText(item), - item, - }), { turnId, itemId }); + this.#emit( + "item.started", + boundedPayload({ + kind: text(item.type, "unknown"), + channel, + providerPhase: text(item.phase) || undefined, + text: itemText(item), + item, + }), + { turnId, itemId }, + ); return; } if (notification.method === "item/completed") { if (!this.#notificationNamesActiveTurn(turnId, "item completion")) return; if (!this.#captureResultFromItem(item, turnId)) return; const channel = itemId - ? this.#itemChannels.get(itemId) ?? this.#channelForStartedItem(item) + ? (this.#itemChannels.get(itemId) ?? this.#channelForStartedItem(item)) : this.#channelForStartedItem(item); - this.#emit("item.completed", boundedPayload({ - kind: text(item.type, "unknown"), - channel, - providerPhase: text(item.phase) || undefined, - text: itemText(item), - item, - }), { turnId, itemId }); + this.#emit( + "item.completed", + boundedPayload({ + kind: text(item.type, "unknown"), + channel, + providerPhase: text(item.phase) || undefined, + text: itemText(item), + item, + }), + { turnId, itemId }, + ); if (text(item.type) === "agentMessage") { for (const reference of paperclipWorkspaceFileReferencesFromText( this.#opened.context.workingDirectory, @@ -1468,11 +1815,16 @@ class CodexHarnessSession implements HarnessSession { )) { if (this.#emittedFileReferences.has(reference.referenceId)) continue; this.#emittedFileReferences.add(reference.referenceId); - this.#emit("workspace.file.referenced", { ...reference }, { turnId, itemId: reference.referenceId }); + this.#emit( + "workspace.file.referenced", + { ...reference }, + { turnId, itemId: reference.referenceId }, + ); } } if (itemId) this.#itemChannels.delete(itemId); - if (text(item.type) === "fileChange") this.#recordWorkspaceChanges(turnId, item.changes, true); + if (text(item.type) === "fileChange") + this.#recordWorkspaceChanges(turnId, item.changes, true); return; } const deltaKinds: Record = { @@ -1490,18 +1842,23 @@ class CodexHarnessSession implements HarnessSession { if (deltaKind !== undefined) { if (!this.#notificationNamesActiveTurn(turnId, "item update")) return; const methodChannel = this.#channelForDelta(notification.method); - const channel = methodChannel !== "unknown" - ? methodChannel - : itemId - ? this.#itemChannels.get(itemId) ?? "unknown" - : "unknown"; - this.#emit("item.delta", boundedPayload({ - kind: deltaKind, - channel, - providerMethod: notification.method, - text: text(params.delta, text(params.patch, text(params.output))), - update: params, - }), { turnId, itemId: itemId || `${turnId}:${deltaKind}` }); + const channel = + methodChannel !== "unknown" + ? methodChannel + : itemId + ? (this.#itemChannels.get(itemId) ?? "unknown") + : "unknown"; + this.#emit( + "item.delta", + boundedPayload({ + kind: deltaKind, + channel, + providerMethod: notification.method, + text: text(params.delta, text(params.patch, text(params.output))), + update: params, + }), + { turnId, itemId: itemId || `${turnId}:${deltaKind}` }, + ); if (notification.method === "item/fileChange/patchUpdated") { this.#recordWorkspaceChanges(turnId, params.changes, false); } @@ -1513,15 +1870,21 @@ class CodexHarnessSession implements HarnessSession { // is being attached, before the next turn has started. Keep the snapshot, // but do not turn that benign replay into a fatal turn-binding violation. if (this.#activeTurnId === null || turnId !== this.#activeTurnId) return; - this.#emit("item.completed", { kind: "usage", usage: this.#usage }, { - turnId, - itemId: `${turnId}:usage:${this.#sourceSequence + 1}`, - }); + this.#emit( + "item.completed", + { kind: "usage", usage: this.#usage }, + { + turnId, + itemId: `${turnId}:usage:${this.#sourceSequence + 1}`, + }, + ); return; } } - #channelForStartedItem(item: Record): "progress" | "final" | "summary" | "detail" | "unknown" { + #channelForStartedItem( + item: Record, + ): "progress" | "final" | "summary" | "detail" | "unknown" { const type = text(item.type); const phase = text(item.phase).toLowerCase(); if (type === "agentMessage") { @@ -1533,63 +1896,101 @@ class CodexHarnessSession implements HarnessSession { return "unknown"; } - #channelForDelta(method: string): "progress" | "final" | "summary" | "detail" | "unknown" { + #channelForDelta( + method: string, + ): "progress" | "final" | "summary" | "detail" | "unknown" { if (method === "item/reasoning/summaryTextDelta") return "summary"; if (method === "item/reasoning/textDelta") return "detail"; return "unknown"; } - #recordWorkspaceChanges(turnId: string, value: unknown, complete: boolean): void { + #recordWorkspaceChanges( + turnId: string, + value: unknown, + complete: boolean, + ): void { const changes = Array.isArray(value) ? value : []; - const files = changes.slice(0, 2_000).flatMap((candidate): Record[] => { - const change = record(candidate); - const path = text(change.path).replaceAll("\\", "/"); - if (!path || path.startsWith("/") || path.split("/").includes("..")) return []; - const kind = change.kind; - const kindRecord = record(kind); - const kindText = text(kind, text(kindRecord.type, Object.keys(kindRecord)[0] ?? "update")); - const update = record(kindRecord.update ?? change.update); - const previousPath = text(update.move_path, text(update.movePath)) || null; - const operation = previousPath - ? "rename" - : kindText.toLowerCase().includes("add") - ? "create" - : kindText.toLowerCase().includes("delete") - ? "delete" - : "modify"; - const diff = text(change.diff).slice(0, 262_144) || null; - const diffLines = diff?.split("\n") ?? []; - return [{ - path, - operation, - previousPath, - additions: diff === null ? null : diffLines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length, - deletions: diff === null ? null : diffLines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length, - binary: diff === null, - diff, - }]; - }); + const files = changes + .slice(0, 2_000) + .flatMap((candidate): Record[] => { + const change = record(candidate); + const path = text(change.path).replaceAll("\\", "/"); + if (!path || path.startsWith("/") || path.split("/").includes("..")) + return []; + const kind = change.kind; + const kindRecord = record(kind); + const kindText = text( + kind, + text(kindRecord.type, Object.keys(kindRecord)[0] ?? "update"), + ); + const update = record(kindRecord.update ?? change.update); + const previousPath = + text(update.move_path, text(update.movePath)) || null; + const operation = previousPath + ? "rename" + : kindText.toLowerCase().includes("add") + ? "create" + : kindText.toLowerCase().includes("delete") + ? "delete" + : "modify"; + const diff = text(change.diff).slice(0, 262_144) || null; + const diffLines = diff?.split("\n") ?? []; + return [ + { + path, + operation, + previousPath, + additions: + diff === null + ? null + : diffLines.filter( + (line) => line.startsWith("+") && !line.startsWith("+++"), + ).length, + deletions: + diff === null + ? null + : diffLines.filter( + (line) => line.startsWith("-") && !line.startsWith("---"), + ).length, + binary: diff === null, + diff, + }, + ]; + }); if (files.length === 0) return; - const unknown = files.some((file) => file.additions === null || file.deletions === null); + const unknown = files.some( + (file) => file.additions === null || file.deletions === null, + ); const payload = { schema: "paperclip.workspace.diff.v1", changeSetId: `${turnId}:workspace`, - revision: Number(record(this.#workspaceChangesByTurn.get(turnId)).revision ?? 0) + 1, + revision: + Number(record(this.#workspaceChangesByTurn.get(turnId)).revision ?? 0) + + 1, source: "harness_reported", complete, files, totals: { files: files.length, - additions: unknown ? null : files.reduce((sum, file) => sum + Number(file.additions), 0), - deletions: unknown ? null : files.reduce((sum, file) => sum + Number(file.deletions), 0), + additions: unknown + ? null + : files.reduce((sum, file) => sum + Number(file.additions), 0), + deletions: unknown + ? null + : files.reduce((sum, file) => sum + Number(file.deletions), 0), }, patchArtifactRef: null, }; this.#workspaceChangesByTurn.set(turnId, payload); - this.#emit("workspace.change.updated", payload, { turnId, itemId: `${turnId}:workspace` }); + this.#emit("workspace.change.updated", payload, { + turnId, + itemId: `${turnId}:workspace`, + }); } - async #handleServerRequest(request: CodexRpcServerRequest): Promise> { + async #handleServerRequest( + request: CodexRpcServerRequest, + ): Promise> { if (request.method === "item/tool/call") { const tool = text(request.params.tool); const threadId = text(request.params.threadId); @@ -1603,16 +2004,32 @@ class CodexHarnessSession implements HarnessSession { turnId !== this.#activeTurnId || callId.length === 0 ) { - this.#failProtocol("tool_binding_mismatch", "Semantic tool call did not name the active thread and turn."); - return rejectedToolCall("Semantic tool call was outside the active thread and turn."); + this.#failProtocol( + "tool_binding_mismatch", + "Semantic tool call did not name the active thread and turn.", + ); + return rejectedToolCall( + "Semantic tool call was outside the active thread and turn.", + ); } if (!isSemanticTool(tool)) { - const admitted = this.#dynamicTools.some((candidate) => candidate.name === tool); + const admitted = this.#dynamicTools.some( + (candidate) => candidate.name === tool, + ); if (admitted && this.#dynamicToolHandler !== undefined) { - this.#emit("item.started", { - kind: "dynamicToolCall", - item: { type: "tool_use", id: callId, name: tool, input: request.params.arguments }, - }, { turnId, itemId: callId }); + this.#emit( + "item.started", + { + kind: "dynamicToolCall", + item: { + type: "tool_use", + id: callId, + name: tool, + input: request.params.arguments, + }, + }, + { turnId, itemId: callId }, + ); try { const result = await this.#dynamicToolHandler({ tool, @@ -1621,17 +2038,38 @@ class CodexHarnessSession implements HarnessSession { turnId, arguments: request.params.arguments, }); - this.#emit("item.completed", { - kind: "dynamicToolCall", - item: { type: "tool_result", id: callId, tool_use_id: callId, result }, - }, { turnId, itemId: callId }); + this.#emit( + "item.completed", + { + kind: "dynamicToolCall", + item: { + type: "tool_result", + id: callId, + tool_use_id: callId, + result, + }, + }, + { turnId, itemId: callId }, + ); return dynamicToolResponse(result); } catch (error) { - const message = boundedText(error instanceof Error ? error.message : error); - this.#emit("item.completed", { - kind: "dynamicToolCall", - item: { type: "tool_result", id: callId, tool_use_id: callId, error: message, is_error: true }, - }, { turnId, itemId: callId }); + const message = boundedText( + error instanceof Error ? error.message : error, + ); + this.#emit( + "item.completed", + { + kind: "dynamicToolCall", + item: { + type: "tool_result", + id: callId, + tool_use_id: callId, + error: message, + is_error: true, + }, + }, + { turnId, itemId: callId }, + ); return { success: false, contentItems: [{ type: "inputText", text: message }], @@ -1642,32 +2080,49 @@ class CodexHarnessSession implements HarnessSession { return rejectedToolCall("Unsupported tool."); } if (!isRetainableCodexPayload(request.params.arguments)) { - return rejectedToolCall("Semantic result exceeded the retained payload limit."); + return rejectedToolCall( + "Semantic result exceeded the retained payload limit.", + ); } - const validation = validatePrpStructuredRunResult(request.params.arguments); + const validation = validatePrpStructuredRunResult( + request.params.arguments, + ); if (!validation.ok) { return { success: false, - contentItems: [{ type: "inputText", text: "Invalid semantic result." }], + contentItems: [ + { type: "inputText", text: "Invalid semantic result." }, + ], }; } - if (!toolAcceptsDisposition(tool, validation.result.reportedWorkDisposition)) { + if ( + !toolAcceptsDisposition(tool, validation.result.reportedWorkDisposition) + ) { return { success: false, - contentItems: [{ - type: "inputText", - text: - tool === CODEX_BLOCK_TOOL_NAME - ? "paperclip_block requires reportedWorkDisposition=blocked." - : "paperclip_finish accepts only done or needs_review.", - }], + contentItems: [ + { + type: "inputText", + text: + tool === CODEX_BLOCK_TOOL_NAME + ? "paperclip_block requires reportedWorkDisposition=blocked." + : "paperclip_finish accepts only done or needs_review.", + }, + ], }; } const admission = this.#admitResult(validation.result, callId, turnId); if (admission === "conflict") { - return rejectedToolCall("A different semantic result was already committed."); + return rejectedToolCall( + "A different semantic result was already committed.", + ); } - return { success: true, contentItems: [{ type: "inputText", text: "Semantic completion accepted." }] }; + return { + success: true, + contentItems: [ + { type: "inputText", text: "Semantic completion accepted." }, + ], + }; } const requestKind = runtimeRequestKind(request.method); @@ -1680,12 +2135,18 @@ class CodexHarnessSession implements HarnessSession { this.#terminal || this.#protocolFailed ) { - this.#failProtocol("runtime_request_binding_mismatch", "Runtime request did not name the active thread and turn."); + this.#failProtocol( + "runtime_request_binding_mismatch", + "Runtime request did not name the active thread and turn.", + ); return safeRequestResponse(request.method); } const requestId = String(request.id); if (this.#pendingRuntimeRequests.has(requestId)) { - this.#failProtocol("runtime_request_duplicate", "Provider reused a pending runtime request identity."); + this.#failProtocol( + "runtime_request_duplicate", + "Provider reused a pending runtime request identity.", + ); return safeRequestResponse(request.method); } const runtimeRequest: HarnessRuntimeRequest = { @@ -1698,20 +2159,34 @@ class CodexHarnessSession implements HarnessSession { prompt: runtimeRequestPrompt(requestKind, request.params), details: record(redactCodexValue(boundedCodexValue(request.params))), }; - this.#emit("runtime_request.created", { - request: { ...runtimeRequest, type: runtimeRequest.method }, - }, { - turnId: requestTurnId, - itemId: runtimeRequest.itemId, - }); + this.#emit( + "runtime_request.created", + { + request: { ...runtimeRequest, type: runtimeRequest.method }, + }, + { + turnId: requestTurnId, + itemId: runtimeRequest.itemId, + }, + ); return new Promise>((settle) => { - this.#pendingRuntimeRequests.set(requestId, { request: runtimeRequest, settle }); + this.#pendingRuntimeRequests.set(requestId, { + request: runtimeRequest, + settle, + }); }); } - #captureResultFromItem(item: Record, turnId: string): boolean { + #captureResultFromItem( + item: Record, + turnId: string, + ): boolean { if (this.#conversationMode === "direct") return true; - if (text(item.type) !== "agentMessage" || !isRetainableCodexPayload(item.text)) return true; + if ( + text(item.type) !== "agentMessage" || + !isRetainableCodexPayload(item.text) + ) + return true; const result = tryParseResult(item.text); if (result !== null && isRetainableCodexPayload(result)) { const admission = this.#admitResult(result, text(item.id), turnId); @@ -1740,14 +2215,18 @@ class CodexHarnessSession implements HarnessSession { this.#resultCallId = itemId || null; this.#resultTurnId = turnId || this.#activeTurnId; result.verification.forEach((verification, index) => { - this.#emit("item.completed", { - kind: "verification", - text: `${verification.status}: ${verification.commandOrCheck}`, - verification, - }, { - turnId: turnId || this.#activeTurnId || undefined, - itemId: `${itemId || "semantic-result"}:verification:${index + 1}`, - }); + this.#emit( + "item.completed", + { + kind: "verification", + text: `${verification.status}: ${verification.commandOrCheck}`, + verification, + }, + { + turnId: turnId || this.#activeTurnId || undefined, + itemId: `${itemId || "semantic-result"}:verification:${index + 1}`, + }, + ); }); this.#emit("run.result.proposed", result, { turnId: turnId || this.#activeTurnId || undefined, @@ -1811,10 +2290,14 @@ class CodexHarnessSession implements HarnessSession { this.#terminalTurns.set(turnId, this.#terminalFingerprint(turn)); const workspace = this.#workspaceChangesByTurn.get(turnId); if (workspace !== undefined) { - this.#emit("workspace.diff.recorded", { ...workspace, complete: true }, { - turnId, - itemId: `${turnId}:workspace`, - }); + this.#emit( + "workspace.diff.recorded", + { ...workspace, complete: true }, + { + turnId, + itemId: `${turnId}:workspace`, + }, + ); } const eventType = status === "failed" @@ -1825,10 +2308,14 @@ class CodexHarnessSession implements HarnessSession { ? "turn.cancelled" : "turn.completed"; this.#cancelPendingRequests("turn_terminal"); - this.#emit(eventType, boundedPayload({ - status, - error: turn.error ?? null, - }), { turnId }); + this.#emit( + eventType, + boundedPayload({ + status, + error: turn.error ?? null, + }), + { turnId }, + ); this.#activeTurnId = null; this.#turnStarted = false; this.#finalize(status); @@ -1861,34 +2348,44 @@ class CodexHarnessSession implements HarnessSession { }; } const semanticResult = this.#result ?? candidate; - if (this.#terminalFingerprint(turn, semanticResult) !== expectedFingerprint) { + if ( + this.#terminalFingerprint(turn, semanticResult) !== expectedFingerprint + ) { return { code: "conflicting_turn_terminal", message: "changed from its committed terminal fingerprint", }; } if (candidate !== null && this.#result === null) { - this.#admitResult(candidate, text(this.#resultItemFromTurn(turn)?.id), text(turn.id)); + this.#admitResult( + candidate, + text(this.#resultItemFromTurn(turn)?.id), + text(turn.id), + ); } return null; } - #resultItemFromTurn(turn: Record): Record | null { + #resultItemFromTurn( + turn: Record, + ): Record | null { if (!Array.isArray(turn.items)) return null; return ( turn.items .map(record) .reverse() - .find((value) => - text(value.type) === "agentMessage" && - isRetainableCodexPayload(value.text) && - tryParseResult(value.text) !== null - ) ?? - null + .find( + (value) => + text(value.type) === "agentMessage" && + isRetainableCodexPayload(value.text) && + tryParseResult(value.text) !== null, + ) ?? null ); } - #resultFromTurn(turn: Record): PrpStructuredRunResult | null { + #resultFromTurn( + turn: Record, + ): PrpStructuredRunResult | null { const item = this.#resultItemFromTurn(turn); return item === null ? null : tryParseResult(item.text); } @@ -1926,17 +2423,24 @@ class CodexHarnessSession implements HarnessSession { this.#activeTurnId === null || turnId !== this.#activeTurnId ) { - this.#failProtocol("turn_binding_mismatch", `Provider ${kind} did not name the active turn.`); + this.#failProtocol( + "turn_binding_mismatch", + `Provider ${kind} did not name the active turn.`, + ); return false; } return true; } #requireCapability(operation: keyof CodexCapabilities): void { - if (!this.#capabilities[operation]) throw this.#unsupported(operation, "capability not advertised"); + if (!this.#capabilities[operation]) + throw this.#unsupported(operation, "capability not advertised"); } - #unsupported(operation: string, detail: unknown): HarnessCapabilityUnavailableError { + #unsupported( + operation: string, + detail: unknown, + ): HarnessCapabilityUnavailableError { const error = new HarnessCapabilityUnavailableError( operation, redactCodexDiagnostic(String(detail)), @@ -1945,7 +2449,10 @@ class CodexHarnessSession implements HarnessSession { return error; } - #diagnoseUnsupported(operation: string, detail = "operation is not available"): void { + #diagnoseUnsupported( + operation: string, + detail = "operation is not available", + ): void { this.#emit("harness.diagnostic", { code: "unsupported_operation", operation, @@ -1966,7 +2473,11 @@ class CodexHarnessSession implements HarnessSession { }); if (!this.#terminal && this.#activeTurnId !== null) { const turnId = this.#activeTurnId; - this.#emit("turn.failed", { status: "failed", error: { code } }, { turnId }); + this.#emit( + "turn.failed", + { status: "failed", error: { code } }, + { turnId }, + ); this.#terminalTurns.set(turnId, canonicalJson({ protocolFailure: code })); this.#activeTurnId = null; } @@ -2018,7 +2529,10 @@ function validateWorkingDirectory( const codexHome = environment.CODEX_HOME?.trim(); if (codexHome) { const resolvedCodexHome = resolve(codexHome); - if (pathContains(resolved, resolvedCodexHome) || pathContains(resolvedCodexHome, resolved)) { + if ( + pathContains(resolved, resolvedCodexHome) || + pathContains(resolvedCodexHome, resolved) + ) { throw new Error("Codex working directory cannot overlap host CODEX_HOME"); } } @@ -2026,8 +2540,13 @@ function validateWorkingDirectory( if (configuredRoot !== undefined && configuredRoot.trim().length > 0) { const root = resolve(configuredRoot); const pathFromRoot = relative(root, resolved); - if (pathFromRoot === ".." || pathFromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) { - throw new Error("Codex working directory is outside the assigned workspace"); + if ( + pathFromRoot === ".." || + pathFromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) + ) { + throw new Error( + "Codex working directory is outside the assigned workspace", + ); } } return resolved; @@ -2035,10 +2554,13 @@ function validateWorkingDirectory( function pathContains(parent: string, candidate: string): boolean { const fromParent = relative(parent, candidate); - return fromParent === "" || ( - fromParent !== ".." && - !fromParent.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && - !parse(fromParent).root + return ( + fromParent === "" || + (fromParent !== ".." && + !fromParent.startsWith( + `..${process.platform === "win32" ? "\\" : "/"}`, + ) && + !parse(fromParent).root) ); } @@ -2053,7 +2575,8 @@ function boundedCodexValue(value: unknown, depth = 0): unknown { const bounded = value .slice(0, 128) .map((entry) => boundedCodexValue(entry, depth + 1)); - if (value.length > 128) bounded.push({ truncated: true, omittedItems: value.length - 128 }); + if (value.length > 128) + bounded.push({ truncated: true, omittedItems: value.length - 128 }); return bounded; } if (typeof value !== "object" || value === null) return value; @@ -2063,20 +2586,26 @@ function boundedCodexValue(value: unknown, depth = 0): unknown { .slice(0, 128) .map(([key, entry]) => [key, boundedCodexValue(entry, depth + 1)]), ); - if (Object.keys(object).length > 128) bounded.truncatedEntries = Object.keys(object).length - 128; + if (Object.keys(object).length > 128) + bounded.truncatedEntries = Object.keys(object).length - 128; const serialized = JSON.stringify(bounded); return Buffer.byteLength(serialized) <= MAX_RETAINED_CODEX_PAYLOAD_BYTES ? bounded : { truncated: true, byteSize: Buffer.byteLength(serialized) }; } -function boundedPayload(value: Record): Record { +function boundedPayload( + value: Record, +): Record { return record(boundedCodexValue(value)); } function isRetainableCodexPayload(value: unknown): boolean { try { - return Buffer.byteLength(JSON.stringify(value)) <= MAX_RETAINED_CODEX_PAYLOAD_BYTES; + return ( + Buffer.byteLength(JSON.stringify(value)) <= + MAX_RETAINED_CODEX_PAYLOAD_BYTES + ); } catch { return false; } @@ -2100,32 +2629,49 @@ function redactCodexValue(value: unknown, depth = 0): unknown { if (depth > 8) return "[TRUNCATED]"; if (typeof value === "string") return redactCodexDiagnostic(value); if (Array.isArray(value)) { - return value.slice(0, 128).map((entry) => redactCodexValue(entry, depth + 1)); + return value + .slice(0, 128) + .map((entry) => redactCodexValue(entry, depth + 1)); } const object = record(value); return Object.fromEntries( - Object.entries(object).slice(0, 128).map(([key, entry]) => [ - key, - /(?:api[_-]?key|token|secret|password|authorization)/i.test(key) - ? "[REDACTED]" - : redactCodexValue(entry, depth + 1), - ]), + Object.entries(object) + .slice(0, 128) + .map(([key, entry]) => [ + key, + /(?:api[_-]?key|token|secret|password|authorization)/i.test(key) + ? "[REDACTED]" + : redactCodexValue(entry, depth + 1), + ]), ); } function rejectedToolCall(message: string): Record { - return { success: false, contentItems: [{ type: "inputText", text: message }] }; + return { + success: false, + contentItems: [{ type: "inputText", text: message }], + }; } function runtimeRequestKind(method: string): HarnessRuntimeRequestKind | null { - if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") { + if ( + method === "item/commandExecution/requestApproval" || + method === "execCommandApproval" + ) { return "command_approval"; } - if (method === "item/fileChange/requestApproval" || method === "applyPatchApproval") { + if ( + method === "item/fileChange/requestApproval" || + method === "applyPatchApproval" + ) { return "file_approval"; } - if (method === "item/permissions/requestApproval") return "permission_approval"; - if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") { + if (method === "item/permissions/requestApproval") + return "permission_approval"; + if ( + method === "item/tool/requestUserInput" || + method === "tool/requestUserInput" + ) { return "user_input"; } if (method === "mcpServer/elicitation/request") return "elicitation"; @@ -2157,9 +2703,14 @@ function runtimeRequestResponse( request: HarnessRuntimeRequest, resolution: HarnessRuntimeRequestResolution, ): Record { - if (request.requestKind === "command_approval" || request.requestKind === "file_approval") { + if ( + request.requestKind === "command_approval" || + request.requestKind === "file_approval" + ) { if (resolution.action === "submit") { - throw new Error(`${request.requestKind} does not accept submitted form data`); + throw new Error( + `${request.requestKind} does not accept submitted form data`, + ); } const decisions = { accept: "accept", @@ -2171,7 +2722,9 @@ function runtimeRequestResponse( } if (request.requestKind === "permission_approval") { if (resolution.action === "submit") { - throw new Error("permission approval does not accept submitted form data"); + throw new Error( + "permission approval does not accept submitted form data", + ); } return { permissions: {}, @@ -2187,9 +2740,16 @@ function runtimeRequestResponse( return { answers: structuredClone(resolution.answers) }; } if (resolution.action === "submit" && "content" in resolution) { - return { action: "accept", content: structuredClone(resolution.content), _meta: null }; + return { + action: "accept", + content: structuredClone(resolution.content), + _meta: null, + }; } - if (resolution.action === "submit" || resolution.action === "accept_for_session") { + if ( + resolution.action === "submit" || + resolution.action === "accept_for_session" + ) { throw new Error("elicitation submissions require content"); } return { action: resolution.action, content: null, _meta: null }; @@ -2203,7 +2763,14 @@ function parseThreadGoal(value: unknown): HarnessThreadGoal | null { if ( threadId.length === 0 || objective.length === 0 || - !["active", "paused", "blocked", "usageLimited", "budgetLimited", "complete"].includes(status) + ![ + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "complete", + ].includes(status) ) { return null; } @@ -2213,7 +2780,8 @@ function parseThreadGoal(value: unknown): HarnessThreadGoal | null { status: status as HarnessThreadGoal["status"], tokenBudget: typeof goal.tokenBudget === "number" ? goal.tokenBudget : null, tokensUsed: typeof goal.tokensUsed === "number" ? goal.tokensUsed : 0, - timeUsedSeconds: typeof goal.timeUsedSeconds === "number" ? goal.timeUsedSeconds : 0, + timeUsedSeconds: + typeof goal.timeUsedSeconds === "number" ? goal.timeUsedSeconds : 0, createdAt: typeof goal.createdAt === "number" ? goal.createdAt : 0, updatedAt: typeof goal.updatedAt === "number" ? goal.updatedAt : 0, }; @@ -2229,18 +2797,31 @@ function lineageFromThread(value: unknown): HarnessThreadLineageEntry { const source = record(thread.source); const subAgent = source.subAgent ?? source.subagent; const subAgentRecord = record(subAgent); - const spawn = record(subAgentRecord.thread_spawn ?? subAgentRecord.threadSpawn); - const parentThreadId = text( - spawn.parent_thread_id ?? spawn.parentThreadId, - text(thread.forkedFromId), - ) || null; + const spawn = record( + subAgentRecord.thread_spawn ?? subAgentRecord.threadSpawn, + ); + const parentThreadId = + text( + spawn.parent_thread_id ?? spawn.parentThreadId, + text(thread.forkedFromId), + ) || null; return { threadId: text(thread.id), providerSessionId: text(thread.sessionId) || null, parentThreadId, - depth: typeof spawn.depth === "number" ? spawn.depth : parentThreadId === null ? 0 : 1, - nickname: text(thread.agentNickname, text(spawn.agent_nickname ?? spawn.agentNickname)) || null, - role: text(thread.agentRole, text(spawn.agent_role ?? spawn.agentRole)) || null, + depth: + typeof spawn.depth === "number" + ? spawn.depth + : parentThreadId === null + ? 0 + : 1, + nickname: + text( + thread.agentNickname, + text(spawn.agent_nickname ?? spawn.agentNickname), + ) || null, + role: + text(thread.agentRole, text(spawn.agent_role ?? spawn.agentRole)) || null, status: threadStatus(thread.status), }; } @@ -2274,17 +2855,27 @@ function isBoundCodexNotification(method: string): boolean { ); } -function safeRequestResponse(method: string, action: "decline" | "cancel" = "decline"): Record { +function safeRequestResponse( + method: string, + action: "decline" | "cancel" = "decline", +): Record { if (method === "item/permissions/requestApproval") { return { permissions: {}, scope: "turn" }; } if (method === "mcpServer/elicitation/request") { return { action, content: null, _meta: null }; } - if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") { + if ( + method === "item/tool/requestUserInput" || + method === "tool/requestUserInput" + ) { return { answers: {} }; } - if (method.includes("requestApproval") || method === "execCommandApproval" || method === "applyPatchApproval") { + if ( + method.includes("requestApproval") || + method === "execCommandApproval" || + method === "applyPatchApproval" + ) { return { decision: action }; } return {}; diff --git a/packages/paperclip-runner/src/index.ts b/packages/paperclip-runner/src/index.ts index 22600963661..6713e91ae9c 100644 --- a/packages/paperclip-runner/src/index.ts +++ b/packages/paperclip-runner/src/index.ts @@ -5,6 +5,7 @@ export * from "./contracts/native-execution.js"; export * from "./contracts/local-runner.js"; export * from "./contracts/durable-recovery.js"; export * from "./contracts/codex.js"; +export * from "./contracts/provider-trace.js"; export * from "./contracts/types.js"; export * from "./backends/harness-driver-backend.js"; export * from "./backends/codex-native-backend.js"; diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index 2f670eb4ec4..407a85fe3e2 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -1,4 +1,5 @@ import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { createHash } from "node:crypto"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -6,6 +7,7 @@ import { expect, it } from "vitest"; import { createCapabilityRunnerdCodexTransport, + createSanitizedClaudeManagedEnvironment, createSanitizedAwsAgentCoreEnvironment, defaultCapabilityRunnerdBinary, rehydrateRunnerdUsageNotification, @@ -13,9 +15,27 @@ import { unwrapRunnerdProviderNotifications, } from "./runnerd-codex-transport.js"; +it("passes only runtime basics and the Anthropic key to a Claude Managed runner", () => { + const sanitized = createSanitizedClaudeManagedEnvironment({ + PATH: "/bin", + HOME: "/Users/tester", + ANTHROPIC_API_KEY: "anthropic-canary", + PAPERCLIP_API_KEY: "paperclip-canary", + PAPERCLIP_API_URL: "https://paperclip.example.test", + OPENROUTER_API_KEY: "openrouter-canary", + AWS_SECRET_ACCESS_KEY: "aws-canary", + }); + expect(sanitized).toEqual({ + PATH: "/bin", + HOME: "/Users/tester", + ANTHROPIC_API_KEY: "anthropic-canary", + }); +}); + it("passes only workload-identity metadata to the AgentCore runner", () => { const sanitized = createSanitizedAwsAgentCoreEnvironment({ PATH: "/bin", + HOME: "/Users/tester", AWS_PROFILE: "paperclip-dev", AWS_REGION: "us-east-1", AWS_WEB_IDENTITY_TOKEN_FILE: "/var/run/token", @@ -28,6 +48,7 @@ it("passes only workload-identity metadata to the AgentCore runner", () => { }); expect(sanitized).toEqual({ PATH: "/bin", + HOME: "/Users/tester", AWS_PROFILE: "paperclip-dev", AWS_REGION: "us-east-1", AWS_WEB_IDENTITY_TOKEN_FILE: "/var/run/token", @@ -36,11 +57,17 @@ it("passes only workload-identity metadata to the AgentCore runner", () => { }); it("rehydrates normalized usage with the provider thread binding", () => { - expect(rehydrateRunnerdUsageNotification({ - providerSessionId: "thread-1", - cumulative: { inputTokens: 10 }, - runDelta: { inputTokens: 3 }, - }, "fallback-thread", "turn-1")).toMatchObject({ + expect( + rehydrateRunnerdUsageNotification( + { + providerSessionId: "thread-1", + cumulative: { inputTokens: 10 }, + runDelta: { inputTokens: 3 }, + }, + "fallback-thread", + "turn-1", + ), + ).toMatchObject({ threadId: "thread-1", turnId: "turn-1", tokenUsage: { @@ -56,27 +83,34 @@ const fakeCodex = resolve( ); it("unwraps a coalesced provider notification without losing its turn identity", () => { - expect(unwrapRunnerdProviderNotification({ - coalescedCount: 2, - latest: { - method: "turn/started", - params: { threadId: "thread-1", turn: { id: "provider-turn-1" } }, - }, - })).toEqual({ + expect( + unwrapRunnerdProviderNotification({ + coalescedCount: 2, + latest: { + method: "turn/started", + params: { threadId: "thread-1", turn: { id: "provider-turn-1" } }, + }, + }), + ).toEqual({ method: "turn/started", params: { threadId: "thread-1", turn: { id: "provider-turn-1" } }, }); }); it("replays every provider notification from a durable coalesced batch", () => { - expect(unwrapRunnerdProviderNotifications({ - coalescedCount: 3, - events: [ - { method: "item/started", params: { item: { id: "reasoning-1" } } }, - { method: "item/reasoning/summaryTextDelta", params: { delta: "Checking the task" } }, - { method: "item/completed", params: { item: { id: "reasoning-1" } } }, - ], - })).toEqual([ + expect( + unwrapRunnerdProviderNotifications({ + coalescedCount: 3, + events: [ + { method: "item/started", params: { item: { id: "reasoning-1" } } }, + { + method: "item/reasoning/summaryTextDelta", + params: { delta: "Checking the task" }, + }, + { method: "item/completed", params: { item: { id: "reasoning-1" } } }, + ], + }), + ).toEqual([ expect.objectContaining({ method: "item/started" }), expect.objectContaining({ method: "item/reasoning/summaryTextDelta" }), expect.objectContaining({ method: "item/completed" }), @@ -91,20 +125,31 @@ it("runs the lab provider boundary through authenticated durable PRP", async () }); bundle.transport.setServerRequestHandler(async (request) => ({ success: true, - contentItems: [{ - type: "inputText", - text: JSON.stringify({ ok: true, result: { task: { title: "PRP lab task" } } }), - }], + contentItems: [ + { + type: "inputText", + text: JSON.stringify({ + ok: true, + result: { task: { title: "PRP lab task" } }, + }), + }, + ], })); try { await bundle.transport.request("initialize", {}); const opened = await bundle.transport.request("thread/start", { cwd: tmpdir(), - dynamicTools: [{ - name: "get_task_context", - description: "Read the active task.", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, - }], + dynamicTools: [ + { + name: "get_task_context", + description: "Read the active task.", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + }, + ], }); expect(opened.thread).toMatchObject({ modelProvider: "openai" }); await bundle.transport.request("turn/start", { @@ -122,7 +167,141 @@ it("runs the lab provider boundary through authenticated durable PRP", async () } finally { await bundle.transport.close(); } - expect(bundle.evidence()).toMatchObject({ runnerExited: true, runnerExitCode: 0 }); + expect(bundle.evidence()).toMatchObject({ + runnerExited: true, + runnerExitCode: 0, + }); +}, 30_000); + +it("captures exact provider frames and correlates Rust and TypeScript interpretation stages", async () => { + const traceDirectory = await mkdtemp( + join(tmpdir(), "runnerd-provider-trace-"), + ); + const tracePath = join(traceDirectory, "trace.ndjson"); + const bundle = createCapabilityRunnerdCodexTransport({ + runnerBinary: defaultCapabilityRunnerdBinary(), + codexCommand: fakeCodex, + codexArgs: [], + stateDirectory: join(traceDirectory, "state"), + environment: { + PAPERCLIP_PROVIDER_TRACE_PATH: tracePath, + PAPERCLIP_PROVIDER_TRACE_MAX_BYTES: String(64 * 1024 * 1024), + }, + }); + bundle.transport.setServerRequestHandler(async () => ({ + success: true, + contentItems: [{ type: "inputText", text: JSON.stringify({ ok: true }) }], + })); + try { + await bundle.transport.request("initialize", {}); + await bundle.transport.request("thread/start", { + cwd: tmpdir(), + dynamicTools: [ + { + name: "get_task_context", + description: "Read the active task.", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + }, + ], + }); + await bundle.transport.request("turn/start", { + input: [{ type: "text", text: "Return a final response." }], + }); + let persistedSequence = 0; + for await (const notification of bundle.transport.notifications()) { + if (notification.paperclipTrace) { + persistedSequence += 1; + bundle.transport.recordTraceInterpretation?.({ + sourceEventId: notification.paperclipTrace.sourceEventId, + sourceEventType: notification.paperclipTrace.sourceEventType, + providerMethod: notification.method, + disposition: "mapped", + emittedEventIds: [`runner:test:${persistedSequence}`], + reason: "Test driver normalized the rehydrated notification", + }); + } + if (notification.method === "turn/completed") break; + } + } finally { + await bundle.transport.close(); + } + + const nativeEntries = (await readFile(tracePath, "utf8")) + .trim() + .split("\n") + .map((line) => JSON.parse(line) as Record); + const rehydratedEntries = (await readFile(`${tracePath}.rehydration`, "utf8")) + .trim() + .split("\n") + .map((line) => JSON.parse(line) as Record); + const frames = nativeEntries.filter((entry) => entry.kind === "frame"); + expect(frames.map((entry) => entry.frameId)).toEqual( + frames.map((_, index) => index + 1), + ); + expect(frames.map((entry) => entry.direction)).toEqual( + expect.arrayContaining(["client_to_provider", "provider_to_client"]), + ); + for (const frame of frames) { + const raw = Buffer.from(String(frame.rawBase64), "base64"); + expect(raw.byteLength).toBe(frame.byteLength); + expect(`sha256:${createHash("sha256").update(raw).digest("hex")}`).toBe( + frame.digest, + ); + } + const decodedFrames = frames.map((frame) => + JSON.parse(Buffer.from(String(frame.rawBase64), "base64").toString("utf8")), + ) as Array>; + expect( + decodedFrames.find((frame) => frame.method === "thread/start"), + ).toMatchObject({ + params: { + config: { include_collaboration_mode_instructions: true }, + }, + }); + const stages = new Set( + [...nativeEntries, ...rehydratedEntries] + .filter((entry) => entry.kind === "interpretation") + .map((entry) => entry.stage), + ); + expect([...stages]).toEqual( + expect.arrayContaining([ + "rust_native_transport", + "rust_jsonrpc_parse", + "rust_durable_normalization", + "typescript_runnerd_rehydration", + "typescript_codex_driver_normalization", + ]), + ); + expect( + rehydratedEntries.some( + (entry) => + entry.stage === "typescript_codex_driver_normalization" && + Array.isArray(entry.emittedEventIds) && + entry.emittedEventIds.some((eventId) => + String(eventId).startsWith("runner:test:"), + ), + ), + ).toBe(true); + for (const channel of ["rust_native", "typescript_runnerd_rehydration"]) { + const channelEntries = [...nativeEntries, ...rehydratedEntries].filter( + (entry) => entry.debugChannel === channel, + ); + expect(channelEntries.map((entry) => entry.debugSequence)).toEqual( + channelEntries.map((_, index) => index + 1), + ); + const status = channelEntries.at(-1); + expect(status).toMatchObject({ + kind: "trace_status", + status: "complete", + acknowledgedDebugSequence: channelEntries.length - 1, + }); + } + + await rm(traceDirectory, { recursive: true, force: true }); }, 30_000); it("steers the active provider turn through the durable PRP command path", async () => { @@ -211,32 +390,50 @@ it("attaches a second governed run to one warm runner and provider process", asy codexArgs: [], lifecyclePolicy: { mode: "warm", idleTimeoutMs: 60_000 }, }); - bundle.transport.setServerRequestHandler(async () => ({ success: true, contentItems: [] })); + bundle.transport.setServerRequestHandler(async () => ({ + success: true, + contentItems: [], + })); try { await bundle.transport.request("initialize", {}); await bundle.transport.request("thread/start", { cwd: tmpdir(), - dynamicTools: [{ - name: "get_task_context", - description: "Read the active task.", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, - }], + dynamicTools: [ + { + name: "get_task_context", + description: "Read the active task.", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + }, + ], }); const runnerPid = bundle.evidence().runnerPid; const providerPid = bundle.evidence().codexPid; - const notifications = bundle.transport.notifications()[Symbol.asyncIterator](); + const notifications = bundle.transport + .notifications() + [Symbol.asyncIterator](); const waitForCompletion = async (label: string) => { const deadline = Date.now() + 5_000; while (Date.now() < deadline) { const next = await Promise.race([ notifications.next(), - new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} notification timeout`)), 1_000)), + new Promise((_, reject) => + setTimeout( + () => reject(new Error(`${label} notification timeout`)), + 1_000, + ), + ), ]); if (next.value?.method === "turn/completed") return; } throw new Error(`${label} completion timeout`); }; - await bundle.transport.request("turn/start", { input: [{ type: "text", text: "first run" }] }); + await bundle.transport.request("turn/start", { + input: [{ type: "text", text: "first run" }], + }); await waitForCompletion("first run"); await bundle.transport.attachRun?.({ @@ -244,10 +441,16 @@ it("attaches a second governed run to one warm runner and provider process", asy turnId: "turn-binding-second", itemId: "item-binding-second", }); - await bundle.transport.request("turn/start", { input: [{ type: "text", text: "second run" }] }); + await bundle.transport.request("turn/start", { + input: [{ type: "text", text: "second run" }], + }); await waitForCompletion("second run"); - expect(bundle.evidence()).toMatchObject({ runnerPid, codexPid: providerPid, runnerExited: false }); + expect(bundle.evidence()).toMatchObject({ + runnerPid, + codexPid: providerPid, + runnerExited: false, + }); } finally { await bundle.transport.close(); } @@ -270,16 +473,33 @@ it("cold-restores a suspended provider session under a new run binding", async ( stateDirectory, lifecyclePolicy: { mode: "per_turn" as const, idleTimeoutMs: null }, }; - const dynamicTools = [{ - name: "get_task_context", - description: "Read the active task.", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, - }]; - const first = createCapabilityRunnerdCodexTransport({ ...options, prpIdentity: baseIdentity }); - first.transport.setServerRequestHandler(async () => ({ success: true, contentItems: [] })); + const dynamicTools = [ + { + name: "get_task_context", + description: "Read the active task.", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + }, + ]; + const first = createCapabilityRunnerdCodexTransport({ + ...options, + prpIdentity: baseIdentity, + }); + first.transport.setServerRequestHandler(async () => ({ + success: true, + contentItems: [], + })); try { - await first.transport.request("thread/start", { cwd: tmpdir(), dynamicTools }); - await first.transport.request("turn/start", { input: [{ type: "text", text: "first process" }] }); + await first.transport.request("thread/start", { + cwd: tmpdir(), + dynamicTools, + }); + await first.transport.request("turn/start", { + input: [{ type: "text", text: "first process" }], + }); for await (const event of first.transport.notifications()) { if (event.method === "turn/completed") break; } @@ -297,24 +517,36 @@ it("cold-restores a suspended provider session under a new run binding", async ( itemId: "item-cold-restored", }, }); - restored.transport.setServerRequestHandler(async () => ({ success: true, contentItems: [] })); + restored.transport.setServerRequestHandler(async () => ({ + success: true, + contentItems: [], + })); try { const read = await restored.transport.request("thread/read", {}); expect(read.thread).toMatchObject({ id: "fake-thread" }); - const durable = JSON.parse(await readFile( - join(stateDirectory, "runner", "runner-state.json"), - "utf8", - )) as { providerToolBridge?: { authorized?: Record } }; - expect(Object.keys(durable.providerToolBridge?.authorized ?? {})).toEqual(expect.arrayContaining([ - "get_task_context", - "paperclip_finish", - "paperclip_block", - ])); - await restored.transport.request("turn/start", { input: [{ type: "text", text: "restored process" }] }); + const durable = JSON.parse( + await readFile( + join(stateDirectory, "runner", "runner-state.json"), + "utf8", + ), + ) as { providerToolBridge?: { authorized?: Record } }; + expect(Object.keys(durable.providerToolBridge?.authorized ?? {})).toEqual( + expect.arrayContaining([ + "get_task_context", + "paperclip_finish", + "paperclip_block", + ]), + ); + await restored.transport.request("turn/start", { + input: [{ type: "text", text: "restored process" }], + }); for await (const event of restored.transport.notifications()) { if (event.method === "turn/completed") break; } - expect(restored.evidence()).toMatchObject({ runnerExited: false, codexPid: expect.any(Number) }); + expect(restored.evidence()).toMatchObject({ + runnerExited: false, + codexPid: expect.any(Number), + }); } finally { await restored.transport.close(); await rm(stateDirectory, { recursive: true, force: true }); @@ -327,29 +559,47 @@ it("rejects the notification stream promptly when runnerd exits after accepting codexCommand: fakeCodex, codexArgs: ["--linger-after-turn-start"], }); - bundle.transport.setServerRequestHandler(async () => ({ success: true, contentItems: [] })); + bundle.transport.setServerRequestHandler(async () => ({ + success: true, + contentItems: [], + })); try { await bundle.transport.request("initialize", {}); await bundle.transport.request("thread/start", { cwd: tmpdir(), - dynamicTools: [{ - name: "get_task_context", - description: "Read the active task.", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, - }], + dynamicTools: [ + { + name: "get_task_context", + description: "Read the active task.", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + }, + ], }); await bundle.transport.request("turn/start", { input: [{ type: "text", text: "Wait for another instruction." }], }); - const notifications = bundle.transport.notifications()[Symbol.asyncIterator](); + const notifications = bundle.transport + .notifications() + [Symbol.asyncIterator](); expect((await notifications.next()).value?.method).toBe("turn/started"); const runnerPid = bundle.evidence().runnerPid; expect(runnerPid).not.toBeNull(); process.kill(runnerPid!, "SIGKILL"); - await expect(Promise.race([ - notifications.next(), - new Promise((_, reject) => setTimeout(() => reject(new Error("notification stream hung")), 2_000)), - ])).rejects.toThrow("native_runner_process_exited"); + await expect( + Promise.race([ + notifications.next(), + new Promise((_, reject) => + setTimeout( + () => reject(new Error("notification stream hung")), + 2_000, + ), + ), + ]), + ).rejects.toThrow("native_runner_process_exited"); } finally { await bundle.transport.close(); } diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index 719f10f3bc7..2ee9a25416e 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -1,5 +1,12 @@ import { createHash, randomUUID } from "node:crypto"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { + appendFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -9,6 +16,7 @@ import type { CodexRpcNotification, CodexRpcServerRequest, CodexServerRequestHandler, + CodexTraceInterpretation, CodexTransportProcessInfo, } from "../drivers/codex/app-server-transport.js"; import { createSanitizedCodexEnvironment } from "../drivers/codex/app-server-transport.js"; @@ -24,8 +32,14 @@ import { waitForProcess, type RunnerProcessHandle, } from "../control-plane/durable-prp-control-plane.js"; -import { resolveQualifiedAcpxProfile, type QualifiedAcpxAgent } from "../drivers/acpx/qualified-profiles.js"; +import { + resolveQualifiedAcpxProfile, + type QualifiedAcpxAgent, +} from "../drivers/acpx/qualified-profiles.js"; import { createSanitizedAcpxEnvironment } from "../drivers/acpx/environment.js"; +import { createSanitizedClaudeManagedEnvironment } from "../drivers/claude-managed/environment.js"; + +export { createSanitizedClaudeManagedEnvironment } from "../drivers/claude-managed/environment.js"; const packageRoot = fileURLToPath(new URL("../..", import.meta.url)); const executableSuffix = process.platform === "win32" ? ".exe" : ""; @@ -36,14 +50,22 @@ function recoveredControlPlaneIdentity( directory: string, desired: DurableRecoveryIdentity, ): DurableRecoveryIdentity { - const stored = record(JSON.parse(readFileSync(resolve(directory, "mock-core-state.json"), "utf8"))); - const identity = record(stored.identity) as unknown as DurableRecoveryIdentity; + const stored = record( + JSON.parse( + readFileSync(resolve(directory, "mock-core-state.json"), "utf8"), + ), + ); + const identity = record( + stored.identity, + ) as unknown as DurableRecoveryIdentity; if ( - identity.runnerInstanceId !== desired.runnerInstanceId - || identity.environmentLeaseId !== desired.environmentLeaseId - || identity.normalizedSessionId !== desired.normalizedSessionId + identity.runnerInstanceId !== desired.runnerInstanceId || + identity.environmentLeaseId !== desired.environmentLeaseId || + identity.normalizedSessionId !== desired.normalizedSessionId ) { - throw new Error("PRP recovery identity does not match the durable session binding"); + throw new Error( + "PRP recovery identity does not match the durable session binding", + ); } return structuredClone(identity); } @@ -62,7 +84,8 @@ export interface CapabilityRunnerdProcessEvidence { agentRuntimeVersion: string | null; acpProtocolVersion: number | null; providerExecutionKind: "local_process" | "remote_service" | null; - providerService: "anthropic_managed_agents" | "aws_bedrock_agentcore_harness" | null; + providerService: + "anthropic_managed_agents" | "aws_bedrock_agentcore_harness" | null; runnerExited: boolean; runnerExitCode: number | null; runnerSignal: NodeJS.Signals | null; @@ -135,7 +158,8 @@ export interface CapabilityRunnerdCodexTransportOptions { }>; } -export type RunnerdCodexTransportOptions = CapabilityRunnerdCodexTransportOptions; +export type RunnerdCodexTransportOptions = + CapabilityRunnerdCodexTransportOptions; export interface CapabilityRunnerdCodexTransport { transport: CodexAppServerTransport; @@ -144,7 +168,9 @@ export interface CapabilityRunnerdCodexTransport { export type RunnerdCodexTransport = CapabilityRunnerdCodexTransport; -export function unwrapRunnerdProviderNotifications(input: unknown): Record[] { +export function unwrapRunnerdProviderNotifications( + input: unknown, +): Record[] { const payload = record(input); if (typeof payload.method === "string") return [payload]; if (Array.isArray(payload.events)) { @@ -156,7 +182,9 @@ export function unwrapRunnerdProviderNotifications(input: unknown): Record { +export function unwrapRunnerdProviderNotification( + input: unknown, +): Record { const notifications = unwrapRunnerdProviderNotifications(input); return notifications.at(-1) ?? record(input); } @@ -179,7 +207,10 @@ class NotificationQueue implements AsyncIterable { waiter.resolve({ value, done: false }); return; } - if (this.#values.length >= MAX_NOTIFICATION_COUNT || this.#bytes + bytes > MAX_NOTIFICATION_BYTES) { + if ( + this.#values.length >= MAX_NOTIFICATION_COUNT || + this.#bytes + bytes > MAX_NOTIFICATION_BYTES + ) { throw new Error("PRP provider notification queue bound exceeded"); } this.#values.push({ value, bytes }); @@ -208,7 +239,9 @@ class NotificationQueue implements AsyncIterable { } if (this.#error !== null) throw this.#error; if (this.#closed) return { value: undefined, done: true }; - return new Promise((resolveValue, reject) => this.#waiters.push({ resolve: resolveValue, reject })); + return new Promise((resolveValue, reject) => + this.#waiters.push({ resolve: resolveValue, reject }), + ); }, }; } @@ -216,10 +249,177 @@ class NotificationQueue implements AsyncIterable { function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) - ? value as Record + ? (value as Record) : {}; } +type PendingTraceRehydration = { + sourceEventId: string; + eventType: string; + visibleNotificationCount: number; +}; + +type PendingDriverTraceInterpretation = CodexTraceInterpretation; + +function locateRunnerdTraceFrame( + tracePath: string, + sourceEventId: string, +): { frameId: number | null; nativeChannelSettled: boolean } { + const lines = readFileSync(tracePath, "utf8").split("\n"); + let frameId: number | null = null; + let nativeChannelSettled = false; + for (let index = lines.length - 1; index >= 0; index -= 1) { + if (!lines[index]?.trim()) continue; + const entry = record(JSON.parse(lines[index]!)); + if (entry.kind === "trace_status" && entry.debugChannel === "rust_native") { + nativeChannelSettled = true; + } + if ( + entry.kind !== "interpretation" || + !Array.isArray(entry.emittedEventIds) + ) { + continue; + } + if ((entry.emittedEventIds as unknown[]).includes(sourceEventId)) { + frameId = typeof entry.frameId === "number" ? entry.frameId : null; + break; + } + } + return { frameId, nativeChannelSettled }; +} + +function appendRunnerdRehydrationTrace( + tracePath: string | undefined, + sourceEventId: string, + eventType: string, + visibleNotificationCount: number, + debugSequence: number, +): "written" | "retry" | "not_applicable" { + if (!tracePath) return "not_applicable"; + if (!existsSync(tracePath)) return "retry"; + try { + const { frameId, nativeChannelSettled } = locateRunnerdTraceFrame( + tracePath, + sourceEventId, + ); + if (frameId === null) + return nativeChannelSettled ? "not_applicable" : "retry"; + appendFileSync( + `${tracePath}.rehydration`, + `${JSON.stringify({ + kind: "interpretation", + schema: "paperclip.provider_trace_interpretation.v1", + debugChannel: "typescript_runnerd_rehydration", + debugSequence, + frameId, + stage: "typescript_runnerd_rehydration", + ruleId: `runnerd.rehydrate.${eventType}`, + disposition: visibleNotificationCount > 0 ? "mapped" : "ignored", + emittedEventIds: [sourceEventId], + droppedFields: [], + fieldMappings: [ + { + inputPath: "sourceEventId", + outputPath: "paperclipTrace.sourceEventId", + action: "copied", + reason: "Preserved the durable event identity while rehydrating the provider notification", + }, + { + inputPath: "eventType", + outputPath: "paperclipTrace.sourceEventType", + action: "renamed", + reason: "Attached the canonical PRP type to the rehydrated notification", + }, + ], + reason: + visibleNotificationCount > 0 + ? "Canonical PRP event was rehydrated into the Codex driver notification contract" + : "Canonical PRP event did not produce a Codex driver notification", + })}\n`, + { encoding: "utf8", mode: 0o600 }, + ); + return "written"; + } catch { + // Debug delivery is deliberately outside run authority and never fails it. + return "retry"; + } +} + +function appendCodexDriverInterpretationTrace( + tracePath: string | undefined, + input: PendingDriverTraceInterpretation, + debugSequence: number, +): "written" | "retry" | "not_applicable" { + if (!tracePath) return "not_applicable"; + if (!existsSync(tracePath)) return "retry"; + try { + const { frameId, nativeChannelSettled } = locateRunnerdTraceFrame( + tracePath, + input.sourceEventId, + ); + if (frameId === null) { + return nativeChannelSettled ? "not_applicable" : "retry"; + } + appendFileSync( + `${tracePath}.rehydration`, + `${JSON.stringify({ + kind: "interpretation", + schema: "paperclip.provider_trace_interpretation.v1", + debugChannel: "typescript_runnerd_rehydration", + debugSequence, + frameId, + stage: "typescript_codex_driver_normalization", + ruleId: `codex_driver.normalize.${input.providerMethod}`, + disposition: input.disposition, + emittedEventIds: input.emittedEventIds, + droppedFields: [], + fieldMappings: [ + { + inputPath: "params", + outputPath: "payload", + action: "normalized", + reason: "Codex notification fields were normalized into canonical PRP event payloads", + }, + ...input.emittedEventIds.map((eventId) => ({ + outputPath: `event:${eventId}`, + action: "derived", + reason: "The driver emitted this canonical PRP event from the normalized notification", + })), + ], + reason: input.reason, + })}\n`, + { encoding: "utf8", mode: 0o600 }, + ); + return "written"; + } catch { + // Debug delivery is deliberately outside run authority and never fails it. + return "retry"; + } +} + +function appendRunnerdTraceStatus( + tracePath: string, + input: { + status: "complete" | "incomplete"; + reason: string | null; + debugSequence: number; + acknowledgedDebugSequence: number; + }, +): void { + appendFileSync( + `${tracePath}.rehydration`, + `${JSON.stringify({ + kind: "trace_status", + debugChannel: "typescript_runnerd_rehydration", + debugSequence: input.debugSequence, + status: input.status, + acknowledgedDebugSequence: input.acknowledgedDebugSequence, + reason: input.reason, + })}\n`, + { encoding: "utf8", mode: 0o600 }, + ); +} + export function rehydrateRunnerdUsageNotification( rawParams: Record, openedThreadId: string, @@ -231,12 +431,14 @@ export function rehydrateRunnerdUsageNotification( // rather than replaying the provider's original threadId field. Rehydrate // the Codex notification contract so the strict driver does not reject // valid accounting as a cross-thread event. - threadId: typeof rawParams.threadId === "string" - ? rawParams.threadId - : typeof rawParams.providerSessionId === "string" - ? rawParams.providerSessionId - : openedThreadId, - turnId: typeof rawParams.turnId === "string" ? rawParams.turnId : activeTurnId, + threadId: + typeof rawParams.threadId === "string" + ? rawParams.threadId + : typeof rawParams.providerSessionId === "string" + ? rawParams.providerSessionId + : openedThreadId, + turnId: + typeof rawParams.turnId === "string" ? rawParams.turnId : activeTurnId, tokenUsage: { total: record(rawParams.cumulative), runDelta: record(rawParams.runDelta), @@ -248,7 +450,9 @@ function commandDigest(value: unknown): string { return `sha256:${createHash("sha256").update(durableRecoveryInternals.canonicalJson(value)).digest("hex")}`; } -function authorizedToolSet(tools: readonly Readonly>[]): Record { +function authorizedToolSet( + tools: readonly Readonly>[], +): Record { const operations = tools.map((tool) => ({ operationId: String(tool.name ?? ""), version: 1, @@ -264,36 +468,49 @@ function authorizedToolSet(tools: readonly Readonly>[]): }; } -function createSanitizedClaudeManagedEnvironment(environment: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv { +export function createSanitizedAwsAgentCoreEnvironment( + environment: NodeJS.ProcessEnv | undefined, +): NodeJS.ProcessEnv { const source = environment ?? process.env; const result: NodeJS.ProcessEnv = {}; for (const key of [ - "PATH", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "SSL_CERT_FILE", "SSL_CERT_DIR", - "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy", - "RUST_BACKTRACE", "ANTHROPIC_API_KEY", + "PATH", + "HOME", + "LANG", + "LC_ALL", + "LC_CTYPE", + "TZ", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "no_proxy", + "RUST_BACKTRACE", + "AWS_PROFILE", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_CONFIG_FILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_ROLE_ARN", + "AWS_ROLE_SESSION_NAME", + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", ]) { if (typeof source[key] === "string") result[key] = source[key]; } return result; } -export function createSanitizedAwsAgentCoreEnvironment(environment: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv { - const source = environment ?? process.env; - const result: NodeJS.ProcessEnv = {}; - for (const key of [ - "PATH", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "SSL_CERT_FILE", "SSL_CERT_DIR", - "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy", - "RUST_BACKTRACE", "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_CONFIG_FILE", - "AWS_SHARED_CREDENTIALS_FILE", "AWS_WEB_IDENTITY_TOKEN_FILE", "AWS_ROLE_ARN", - "AWS_ROLE_SESSION_NAME", "AWS_CONTAINER_CREDENTIALS_FULL_URI", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", - ]) { - if (typeof source[key] === "string") result[key] = source[key]; - } - return result; -} - -function unwrapToolResponse(response: Record): Record { - const items = Array.isArray(response.contentItems) ? response.contentItems : []; +function unwrapToolResponse( + response: Record, +): Record { + const items = Array.isArray(response.contentItems) + ? response.contentItems + : []; const value = record(items[0]).text; let result: unknown = response; try { @@ -316,7 +533,12 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { readonly #evidence: CapabilityRunnerdProcessEvidence; #handler: CodexServerRequestHandler = async () => ({ success: false, - contentItems: [{ type: "inputText", text: "No Paperclip control-plane tool handler is installed." }], + contentItems: [ + { + type: "inputText", + text: "No Paperclip control-plane tool handler is installed.", + }, + ], }); #core: DurablePrpControlPlane | null = null; #handle: RunnerProcessHandle | null = null; @@ -330,10 +552,16 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { #closed = false; #failure: Error | null = null; #controlPlaneRelease: (() => Promise | void) | null = null; + #nextTraceDebugSequence = 1; + #traceRehydrationSpoolOverflow = false; + #pendingTraceRehydrations: PendingTraceRehydration[] = []; + #pendingDriverTraceInterpretations: PendingDriverTraceInterpretation[] = []; constructor(readonly options: CapabilityRunnerdCodexTransportOptions) { this.#ownsRoot = options.stateDirectory === undefined; - this.#root = options.stateDirectory ?? mkdtempSync(resolve(tmpdir(), "paperclip-runner-lab-prp-")); + this.#root = + options.stateDirectory ?? + mkdtempSync(resolve(tmpdir(), "paperclip-runner-lab-prp-")); if (options.resumeDynamicTools !== undefined) { this.#authorizedTools = authorizedToolSet([ ...options.resumeDynamicTools, @@ -364,9 +592,12 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { ? createSanitizedClaudeManagedEnvironment(options.environment) : options.provider === "aws_agentcore" ? createSanitizedAwsAgentCoreEnvironment(options.environment) - : options.provider === "acpx" - ? createSanitizedAcpxEnvironment(options.environment, options.acpxAgent ?? "pi") - : createSanitizedCodexEnvironment(options.environment), + : options.provider === "acpx" + ? createSanitizedAcpxEnvironment( + options.environment, + options.acpxAgent ?? "pi", + ) + : createSanitizedCodexEnvironment(options.environment), ).sort(), diagnostics: ["lab transport selected authenticated durable PRP"], }; @@ -376,7 +607,10 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { return structuredClone(this.#evidence); } - async request(method: string, params: Record): Promise> { + async request( + method: string, + params: Record, + ): Promise> { if (this.#closed) throw new Error("PRP Codex transport is closed"); this.#throwIfFailed(); if (method === "initialize") return { user: {} }; @@ -385,8 +619,18 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { // runnerd already negotiated the real preset with the provider during // session.open. This transport-level mask confirms that closed boundary; // turn/start remains runner-managed and never forwards this sentinel. - return this.options.provider === undefined || this.options.provider === "codex" - ? { data: [{ name: "Plan", mode: "plan", model: "runner-managed", reasoning_effort: null }] } + return this.options.provider === undefined || + this.options.provider === "codex" + ? { + data: [ + { + name: "Plan", + mode: "plan", + model: "runner-managed", + reasoning_effort: null, + }, + ], + } : { data: [] }; } if (method === "turn/start") return this.#startTurn(params); @@ -425,7 +669,11 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { if (method === "thread/read") { if (this.#core === null) await this.#resume(); const result = await this.#commandResult("provider.thread.read", {}); - if (this.options.provider !== "claude_managed" && this.options.provider !== "aws_agentcore") return result; + if ( + this.options.provider !== "claude_managed" && + this.options.provider !== "aws_agentcore" + ) + return result; const session = record(result.session); return { ...result, @@ -445,8 +693,11 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { await this.#command("session.destroy", params); return {}; } - if (method === "thread/resume") return { thread: { id: this.#threadId, sessionId: this.#sessionId } }; - throw new Error(`PRP Codex transport does not expose provider method ${method}`); + if (method === "thread/resume") + return { thread: { id: this.#threadId, sessionId: this.#sessionId } }; + throw new Error( + `PRP Codex transport does not expose provider method ${method}`, + ); } notify(_method: string, _params?: Record): void {} @@ -459,6 +710,26 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { this.#handler = handler; } + recordTraceInterpretation(input: CodexTraceInterpretation): void { + const tracePath = this.options.environment?.PAPERCLIP_PROVIDER_TRACE_PATH; + if (!tracePath) return; + const traceResult = appendCodexDriverInterpretationTrace( + tracePath, + input, + this.#nextTraceDebugSequence, + ); + if (traceResult === "written") { + this.#nextTraceDebugSequence += 1; + } else if ( + traceResult === "retry" && + this.#pendingDriverTraceInterpretations.length < 4_096 + ) { + this.#pendingDriverTraceInterpretations.push(structuredClone(input)); + } else if (traceResult === "retry") { + this.#traceRehydrationSpoolOverflow = true; + } + } + processInfo(): CodexTransportProcessInfo { return { pid: this.#evidence.runnerPid, @@ -470,13 +741,20 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { }; } - async attachRun(input: { runId: string; turnId: string; itemId: string }): Promise { - if (this.#closed || this.#core === null) throw new Error("PRP provider session is not attachable"); + async attachRun(input: { + runId: string; + turnId: string; + itemId: string; + }): Promise { + if (this.#closed || this.#core === null) + throw new Error("PRP provider session is not attachable"); await this.#command("run.attach", { runId: input.runId, turnId: input.turnId, itemId: input.itemId, - ...(this.#authorizedTools === null ? {} : { authorizedTools: this.#authorizedTools }), + ...(this.#authorizedTools === null + ? {} + : { authorizedTools: this.#authorizedTools }), }); await this.#waitForAttachment(input.runId); this.#durableTurnId = input.turnId; @@ -488,46 +766,89 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { const attachDeadline = Date.now() + 10_000; let attachmentDrained = false; while (core !== null && Date.now() < attachDeadline) { - const durable = record(JSON.parse(readFileSync(resolve(this.#root, "runner", "runner-state.json"), "utf8"))); - const attached = core.store.state.committedEvents.find((event) => - event.eventType === "run.attached" - && record(record(event.envelope.payload).payload).runId === runId + const durable = record( + JSON.parse( + readFileSync( + resolve(this.#root, "runner", "runner-state.json"), + "utf8", + ), + ), ); - if (attached && Number(durable.ackedSourceSeq ?? 0) >= attached.sourceSeq) { + const attached = core.store.state.committedEvents.find( + (event) => + event.eventType === "run.attached" && + record(record(event.envelope.payload).payload).runId === runId, + ); + if ( + attached && + Number(durable.ackedSourceSeq ?? 0) >= attached.sourceSeq + ) { attachmentDrained = true; break; } this.#throwIfFailed(); await new Promise((resolveWait) => setTimeout(resolveWait, 10)); } - if (!attachmentDrained) throw new Error("runnerd did not durably acknowledge the run attachment"); + if (!attachmentDrained) + throw new Error("runnerd did not durably acknowledge the run attachment"); } async close(): Promise { if (this.#closed) return; this.#closed = true; if (this.#core !== null && this.#handle !== null) { - const perTurnAlreadySuspending = this.options.lifecyclePolicy?.mode === "per_turn" - && this.#core.store.state.committedEvents.some((event) => - event.eventType === "runner.suspending" - || event.eventType === "turn.completed" - || event.eventType === "turn.failed" - || event.eventType === "turn.interrupted" - || event.eventType === "turn.cancelled" + const perTurnAlreadySuspending = + this.options.lifecyclePolicy?.mode === "per_turn" && + this.#core.store.state.committedEvents.some( + (event) => + event.eventType === "runner.suspending" || + event.eventType === "turn.completed" || + event.eventType === "turn.failed" || + event.eventType === "turn.interrupted" || + event.eventType === "turn.cancelled", ); if (!perTurnAlreadySuspending) { this.#core.queueCommand("runner.suspend", {}, undefined, true); } try { - const result = await waitForProcess(this.#handle, this.options.closeGraceMs ?? 10_000); + const result = await waitForProcess( + this.#handle, + this.options.closeGraceMs ?? 10_000, + ); this.#evidence.runnerExited = true; this.#evidence.runnerExitCode = result.code; this.#evidence.runnerSignal = result.signal as NodeJS.Signals | null; - if (result.stderr.trim()) this.#diagnostic(result.stderr.trim().slice(-4_096)); + if (result.stderr.trim()) + this.#diagnostic(result.stderr.trim().slice(-4_096)); } catch (error) { this.#diagnostic(`runner shutdown failed: ${String(error)}`); } } + this.#flushPendingTraceRehydrations(); + const tracePath = this.options.environment?.PAPERCLIP_PROVIDER_TRACE_PATH; + if (tracePath) { + const incomplete = + this.#traceRehydrationSpoolOverflow || + this.#pendingTraceRehydrations.length > 0 || + this.#pendingDriverTraceInterpretations.length > 0; + const debugSequence = this.#nextTraceDebugSequence++; + try { + appendRunnerdTraceStatus(tracePath, { + status: incomplete ? "incomplete" : "complete", + reason: incomplete + ? this.#traceRehydrationSpoolOverflow + ? "typescript_rehydration_spool_full" + : "typescript_rehydration_correlation_incomplete" + : null, + debugSequence, + acknowledgedDebugSequence: debugSequence - 1, + }); + } catch { + // Raw trace failure is intentionally independent of run authority. + } + this.#pendingTraceRehydrations = []; + this.#pendingDriverTraceInterpretations = []; + } if (this.#pump !== null) clearInterval(this.#pump); this.#pump = null; this.#queue.close(); @@ -539,8 +860,11 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { this.#publish(); } - async #start(params: Record): Promise> { - if (this.#core !== null) throw new Error("PRP provider thread is already started"); + async #start( + params: Record, + ): Promise> { + if (this.#core !== null) + throw new Error("PRP provider thread is already started"); const token = randomUUID().replaceAll("-", ""); const identity = this.options.prpIdentity ?? { runnerInstanceId: `runner_lab_${token}`, @@ -551,104 +875,148 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { itemId: `item_lab_${token}`, }; this.#durableTurnId = identity.turnId; - const dynamicTools = Array.isArray(params.dynamicTools) ? params.dynamicTools.map(record) : []; + const dynamicTools = Array.isArray(params.dynamicTools) + ? params.dynamicTools.map(record) + : []; const core = new DurablePrpControlPlane({ stateDirectory: resolve(this.#root, "control-plane"), identity, - onSemanticToolInput: async (call) => unwrapToolResponse(await this.#handler({ - id: call.callId, - method: "item/tool/call", - params: { - threadId: this.#threadId, - turnId: this.#turnId, - callId: call.callId, - tool: call.operationId, - arguments: call.input, - }, - })), + onSemanticToolInput: async (call) => + unwrapToolResponse( + await this.#handler({ + id: call.callId, + method: "item/tool/call", + params: { + threadId: this.#threadId, + turnId: this.#turnId, + callId: call.callId, + tool: call.operationId, + arguments: call.input, + }, + }), + ), connectionLeaseTtlMs: 60 * 60 * 1_000, }); this.#core = core; mkdirSync(resolve(this.#root, "runner"), { recursive: true, mode: 0o700 }); const provider = this.options.provider ?? "codex"; - const opencodeProxyPath = this.options.opencodeProxyPath - ?? fileURLToPath(new URL("../cli/opencode-app-server-proxy.js", import.meta.url)); - const acpxSidecarPath = this.options.acpxSidecarPath - ?? fileURLToPath(new URL("../cli/acpx-runtime-sidecar.js", import.meta.url)); + const opencodeProxyPath = + this.options.opencodeProxyPath ?? + fileURLToPath( + new URL("../cli/opencode-app-server-proxy.js", import.meta.url), + ); + const acpxSidecarPath = + this.options.acpxSidecarPath ?? + fileURLToPath(new URL("../cli/acpx-runtime-sidecar.js", import.meta.url)); this.#authorizedTools = authorizedToolSet(dynamicTools); const managed = this.options.managedProfile; const agentCore = this.options.agentCoreProfile; if (provider === "claude_managed" && managed === undefined) { - throw new Error("Claude Agent runner transport requires a managed profile snapshot"); + throw new Error( + "Claude Agent runner transport requires a managed profile snapshot", + ); } if (provider === "aws_agentcore" && agentCore === undefined) { - throw new Error("AWS AgentCore runner transport requires a qualified profile snapshot"); + throw new Error( + "AWS AgentCore runner transport requires a qualified profile snapshot", + ); } - const acpxAgent = provider === "acpx" ? this.options.acpxAgent ?? "pi" : null; + const acpxAgent = + provider === "acpx" ? (this.options.acpxAgent ?? "pi") : null; const requestedModel = typeof params.model === "string" ? params.model : ""; - const acpxProfile = provider === "acpx" - ? resolveQualifiedAcpxProfile(acpxAgent!, requestedModel) - : null; + const acpxProfile = + provider === "acpx" + ? resolveQualifiedAcpxProfile(acpxAgent!, requestedModel) + : null; core.queueCommand("run.prepare", { authorizedTools: this.#authorizedTools, - provider: provider === "claude_managed" ? { - kind: "claude_managed", - model: managed!.model, - profileId: managed!.profileId, - anthropicAgentId: managed!.anthropicAgentId, - agentVersion: managed!.agentVersion, - environmentId: managed!.environmentId, - betaVersion: managed!.betaVersion, - maxSessionListCostUsd: managed!.maxSessionListCostUsd, - } : provider === "aws_agentcore" ? { - kind: "aws_agentcore", - model: agentCore!.model, - profileId: agentCore!.profileId, - region: agentCore!.region, - accountId: agentCore!.accountId, - harnessArn: agentCore!.harnessArn, - harnessVersion: agentCore!.harnessVersion, - endpointArn: agentCore!.endpointArn, - endpointQualifier: agentCore!.endpointQualifier, - agentRuntimeArn: agentCore!.agentRuntimeArn, - memoryArn: agentCore!.memoryArn, - memoryId: agentCore!.memoryId, - invocationRoleArn: agentCore!.invocationRoleArn, - qualificationRevision: agentCore!.qualificationRevision, - eventExpiryDays: agentCore!.eventExpiryDays, - maxEstimatedSessionCostUsd: agentCore!.maxEstimatedSessionCostUsd, - maxIterations: agentCore!.maxIterations, - maxOutputTokens: agentCore!.maxOutputTokens, - timeoutSeconds: agentCore!.timeoutSeconds, - } : provider === "acpx" ? { - kind: "acpx", - agent: acpxProfile!.agent, - model: requestedModel, - acpxVersion: acpxProfile!.acpxVersion, - agentServerPackage: acpxProfile!.agentServerPackage, - agentServerVersion: acpxProfile!.agentServerVersion, - agentRuntimePackage: acpxProfile!.agentRuntimePackage, - agentRuntimeVersion: acpxProfile!.agentRuntimeVersion, - commandDigest: acpxProfile!.commandDigest, - sidecarCommand: process.execPath, - sidecarArgs: [acpxSidecarPath], - runtimeDirectory: this.options.acpxRuntimeDirectory ?? resolve(this.#root, "acpx"), - normalizedSessionId: identity.normalizedSessionId, - runId: identity.runId, - cwd: String(params.cwd ?? tmpdir()), - instructions: String(params.baseInstructions ?? "You are a Paperclip agent."), - permissionPolicy: "interactive", - } : { - kind: provider, - command: provider === "opencode" ? process.execPath : this.options.codexCommand ?? "codex", - args: provider === "opencode" ? [opencodeProxyPath] : this.options.codexArgs ?? createIsolatedCodexAppServerArgs(this.options.environment), - cwd: String(params.cwd ?? tmpdir()), - model: typeof params.model === "string" ? params.model : null, - instructions: String(params.baseInstructions ?? "You are a Paperclip agent."), - collaborationMode: params.permissions === "paperclip-runner-workspace-read-only" - ? "plan" - : "default", - }, + provider: + provider === "claude_managed" + ? { + kind: "claude_managed", + model: managed!.model, + profileId: managed!.profileId, + anthropicAgentId: managed!.anthropicAgentId, + agentVersion: managed!.agentVersion, + environmentId: managed!.environmentId, + betaVersion: managed!.betaVersion, + maxSessionListCostUsd: managed!.maxSessionListCostUsd, + } + : provider === "aws_agentcore" + ? { + kind: "aws_agentcore", + model: agentCore!.model, + profileId: agentCore!.profileId, + region: agentCore!.region, + accountId: agentCore!.accountId, + harnessArn: agentCore!.harnessArn, + harnessVersion: agentCore!.harnessVersion, + endpointArn: agentCore!.endpointArn, + endpointQualifier: agentCore!.endpointQualifier, + agentRuntimeArn: agentCore!.agentRuntimeArn, + memoryArn: agentCore!.memoryArn, + memoryId: agentCore!.memoryId, + invocationRoleArn: agentCore!.invocationRoleArn, + qualificationRevision: agentCore!.qualificationRevision, + eventExpiryDays: agentCore!.eventExpiryDays, + maxEstimatedSessionCostUsd: + agentCore!.maxEstimatedSessionCostUsd, + maxIterations: agentCore!.maxIterations, + maxOutputTokens: agentCore!.maxOutputTokens, + timeoutSeconds: agentCore!.timeoutSeconds, + } + : provider === "acpx" + ? { + kind: "acpx", + agent: acpxProfile!.agent, + model: requestedModel, + acpxVersion: acpxProfile!.acpxVersion, + agentServerPackage: acpxProfile!.agentServerPackage, + agentServerVersion: acpxProfile!.agentServerVersion, + agentRuntimePackage: acpxProfile!.agentRuntimePackage, + agentRuntimeVersion: acpxProfile!.agentRuntimeVersion, + commandDigest: acpxProfile!.commandDigest, + sidecarCommand: process.execPath, + sidecarArgs: [acpxSidecarPath], + runtimeDirectory: + this.options.acpxRuntimeDirectory ?? + resolve(this.#root, "acpx"), + normalizedSessionId: identity.normalizedSessionId, + runId: identity.runId, + cwd: String(params.cwd ?? tmpdir()), + instructions: String( + params.baseInstructions ?? "You are a Paperclip agent.", + ), + permissionPolicy: "interactive", + } + : { + kind: provider, + command: + provider === "opencode" + ? process.execPath + : (this.options.codexCommand ?? "codex"), + args: + provider === "opencode" + ? [opencodeProxyPath] + : (this.options.codexArgs ?? + createIsolatedCodexAppServerArgs( + this.options.environment, + )), + cwd: String(params.cwd ?? tmpdir()), + model: typeof params.model === "string" ? params.model : null, + instructions: String( + params.baseInstructions ?? "You are a Paperclip agent.", + ), + collaborationMode: + params.permissions === + "paperclip-runner-workspace-read-only" + ? "plan" + : "default", + includeCollaborationModeInstructions: + provider === "codex" && + record(params.config) + .include_collaboration_mode_instructions !== false, + }, }); core.queueCommand("session.open", { reuse: "same_session" }); const registration = this.options.controlPlaneRegistration @@ -665,29 +1033,41 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { p0ReserveBytes: 64 * 1024, maxRuntimeMs: 60 * 60 * 1_000, lifecyclePolicy: this.options.lifecyclePolicy, - runnerBinaryPath: this.options.runnerBinary ?? defaultCapabilityRunnerdBinary(), - environment: provider === "opencode" - ? { - ...process.env, - ...this.options.environment, - PAPERCLIP_OPENCODE_COMMAND: this.options.opencodeCommand ?? "opencode", - PAPERCLIP_OPENCODE_RUNTIME_DIR: this.options.opencodeRuntimeDirectory ?? resolve(this.#root, "opencode"), - PAPERCLIP_RUNNER_INSTANCE_ID: identity.runnerInstanceId, - PAPERCLIP_RUN_ID: identity.runId, - PAPERCLIP_NORMALIZED_SESSION_ID: identity.normalizedSessionId, - } - : provider === "acpx" + runnerBinaryPath: + this.options.runnerBinary ?? defaultCapabilityRunnerdBinary(), + environment: + provider === "opencode" ? { - ...createSanitizedAcpxEnvironment(this.options.environment, acpxAgent!), + ...process.env, + ...this.options.environment, + PAPERCLIP_OPENCODE_COMMAND: + this.options.opencodeCommand ?? "opencode", + PAPERCLIP_OPENCODE_RUNTIME_DIR: + this.options.opencodeRuntimeDirectory ?? + resolve(this.#root, "opencode"), PAPERCLIP_RUNNER_INSTANCE_ID: identity.runnerInstanceId, PAPERCLIP_RUN_ID: identity.runId, PAPERCLIP_NORMALIZED_SESSION_ID: identity.normalizedSessionId, } - : provider === "claude_managed" - ? createSanitizedClaudeManagedEnvironment(this.options.environment) - : provider === "aws_agentcore" - ? createSanitizedAwsAgentCoreEnvironment(this.options.environment) - : this.options.environment, + : provider === "acpx" + ? { + ...createSanitizedAcpxEnvironment( + this.options.environment, + acpxAgent!, + ), + PAPERCLIP_RUNNER_INSTANCE_ID: identity.runnerInstanceId, + PAPERCLIP_RUN_ID: identity.runId, + PAPERCLIP_NORMALIZED_SESSION_ID: identity.normalizedSessionId, + } + : provider === "claude_managed" + ? createSanitizedClaudeManagedEnvironment( + this.options.environment, + ) + : provider === "aws_agentcore" + ? createSanitizedAwsAgentCoreEnvironment( + this.options.environment, + ) + : this.options.environment, }); this.#handle = handle; this.#watchRunner(handle); @@ -704,42 +1084,56 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { id: this.#threadId, sessionId: this.#sessionId, model: params.model, - modelProvider: provider === "claude_managed" - ? "anthropic" - : provider === "aws_agentcore" - ? "amazon-bedrock" - : provider === "opencode" && typeof params.model === "string" - ? params.model.split("/", 1)[0] - : provider === "acpx" - ? acpxAgent === "pi" ? "openrouter" : acpxAgent === "claude" ? "anthropic" : "openai" - : "openai", + modelProvider: + provider === "claude_managed" + ? "anthropic" + : provider === "aws_agentcore" + ? "amazon-bedrock" + : provider === "opencode" && typeof params.model === "string" + ? params.model.split("/", 1)[0] + : provider === "acpx" + ? acpxAgent === "pi" + ? "openrouter" + : acpxAgent === "claude" + ? "anthropic" + : "openai" + : "openai", }, }; } async #resume(): Promise { const desiredIdentity = this.options.prpIdentity; - if (desiredIdentity === undefined || !existsSync(resolve(this.#root, "runner", "runner-state.json"))) { + if ( + desiredIdentity === undefined || + !existsSync(resolve(this.#root, "runner", "runner-state.json")) + ) { throw new Error("PRP provider resume state is unavailable"); } const controlPlaneDirectory = resolve(this.#root, "control-plane"); - const identity = recoveredControlPlaneIdentity(controlPlaneDirectory, desiredIdentity); + const identity = recoveredControlPlaneIdentity( + controlPlaneDirectory, + desiredIdentity, + ); this.#durableTurnId = identity.turnId; const attachingNewRun = identity.runId !== desiredIdentity.runId; const core = new DurablePrpControlPlane({ stateDirectory: controlPlaneDirectory, identity, - onSemanticToolInput: async (call) => unwrapToolResponse(await this.#handler({ - id: call.callId, - method: "item/tool/call", - params: { - threadId: this.#threadId, - turnId: this.#turnId, - callId: call.callId, - tool: call.operationId, - arguments: call.input, - }, - })), + onSemanticToolInput: async (call) => + unwrapToolResponse( + await this.#handler({ + id: call.callId, + method: "item/tool/call", + params: { + threadId: this.#threadId, + turnId: this.#turnId, + callId: call.callId, + tool: call.operationId, + arguments: call.input, + }, + }), + ), connectionLeaseTtlMs: 60 * 60 * 1_000, }); this.#core = core; @@ -758,14 +1152,19 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { p0ReserveBytes: 64 * 1024, maxRuntimeMs: 60 * 60 * 1_000, lifecyclePolicy: this.options.lifecyclePolicy, - runnerBinaryPath: this.options.runnerBinary ?? defaultCapabilityRunnerdBinary(), - environment: this.options.provider === "claude_managed" - ? createSanitizedClaudeManagedEnvironment(this.options.environment) - : this.options.provider === "aws_agentcore" - ? createSanitizedAwsAgentCoreEnvironment(this.options.environment) - : this.options.provider === "acpx" - ? createSanitizedAcpxEnvironment(this.options.environment, this.options.acpxAgent ?? "pi") - : this.options.environment, + runnerBinaryPath: + this.options.runnerBinary ?? defaultCapabilityRunnerdBinary(), + environment: + this.options.provider === "claude_managed" + ? createSanitizedClaudeManagedEnvironment(this.options.environment) + : this.options.provider === "aws_agentcore" + ? createSanitizedAwsAgentCoreEnvironment(this.options.environment) + : this.options.provider === "acpx" + ? createSanitizedAcpxEnvironment( + this.options.environment, + this.options.acpxAgent ?? "pi", + ) + : this.options.environment, }); this.#handle = handle; this.#watchRunner(handle); @@ -775,21 +1174,37 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { this.#pump = setInterval(() => this.#pumpEventsSafely(), 5); await this.#waitForProviderIdentity(); if ( - attachingNewRun - || identity.turnId !== desiredIdentity.turnId - || identity.itemId !== desiredIdentity.itemId + attachingNewRun || + identity.turnId !== desiredIdentity.turnId || + identity.itemId !== desiredIdentity.itemId ) { const priorEvidenceSeq = core.store.state.ackedSourceSeq; const drainDeadline = Date.now() + 10_000; while (Date.now() < drainDeadline) { - const durable = record(JSON.parse(readFileSync(resolve(this.#root, "runner", "runner-state.json"), "utf8"))); + const durable = record( + JSON.parse( + readFileSync( + resolve(this.#root, "runner", "runner-state.json"), + "utf8", + ), + ), + ); if (Number(durable.ackedSourceSeq ?? 0) >= priorEvidenceSeq) break; this.#throwIfFailed(); await new Promise((resolveWait) => setTimeout(resolveWait, 10)); } - const drained = record(JSON.parse(readFileSync(resolve(this.#root, "runner", "runner-state.json"), "utf8"))); + const drained = record( + JSON.parse( + readFileSync( + resolve(this.#root, "runner", "runner-state.json"), + "utf8", + ), + ), + ); if (Number(drained.ackedSourceSeq ?? 0) < priorEvidenceSeq) { - throw new Error("runnerd did not drain prior-run evidence before attachment"); + throw new Error( + "runnerd did not drain prior-run evidence before attachment", + ); } await this.attachRun({ runId: desiredIdentity.runId, @@ -797,12 +1212,18 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { itemId: desiredIdentity.itemId, }); } - this.#diagnostic("runnerd restored its durable PRP session and provider thread"); + this.#diagnostic( + "runnerd restored its durable PRP session and provider thread", + ); } - async #startTurn(params: Record): Promise> { + async #startTurn( + params: Record, + ): Promise> { const input = Array.isArray(params.input) ? params.input.map(record) : []; - const message = input.map((item) => typeof item.text === "string" ? item.text : "").join("\n"); + const message = input + .map((item) => (typeof item.text === "string" ? item.text : "")) + .join("\n"); const pendingTurnId = `turn_lab_${randomUUID().replaceAll("-", "")}`; this.#turnId = pendingTurnId; await this.#command("turn.start", { text: message }); @@ -814,10 +1235,12 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { this.#throwIfFailed(); this.#pumpEvents(); if (this.#turnId !== pendingTurnId) break; - if (this.#handle?.child.exitCode !== null) throw new Error("runnerd exited before provider turn startup"); + if (this.#handle?.child.exitCode !== null) + throw new Error("runnerd exited before provider turn startup"); await new Promise((resolveWait) => setTimeout(resolveWait, 10)); } - if (this.#turnId === pendingTurnId) throw new Error("runnerd did not report the provider turn identity"); + if (this.#turnId === pendingTurnId) + throw new Error("runnerd did not report the provider turn identity"); return { turn: { id: this.#turnId, status: "inProgress" } }; } @@ -853,7 +1276,10 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { await this.#waitCommand(type, commandId); } - async #commandResult(type: string, payload: Record): Promise> { + async #commandResult( + type: string, + payload: Record, + ): Promise> { const core = this.#core; if (core === null) throw new Error("PRP provider thread is not started"); const commandId = `command_lab_${randomUUID().replaceAll("-", "")}`; @@ -864,9 +1290,14 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { this.#throwIfFailed(); const event = core.store.state.committedEvents.find((candidate) => { if (candidate.eventType !== "provider.rpc_result") return false; - return record(record(candidate.envelope.payload).payload).commandId === commandId; + return ( + record(record(candidate.envelope.payload).payload).commandId === + commandId + ); }); - const result = record(record(record(event?.envelope.payload).payload).result); + const result = record( + record(record(event?.envelope.payload).payload).result, + ); if (event !== undefined) return result; await new Promise((resolveWait) => setTimeout(resolveWait, 10)); } @@ -879,10 +1310,13 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { this.#throwIfFailed(); this.#pumpEvents(); if ( - this.#threadId.length > 0 - && (this.#evidence.providerExecutionKind === "remote_service" || this.#evidence.providerPid !== null) - ) return; - if (this.#handle?.child.exitCode !== null) throw new Error("runnerd exited before provider startup"); + this.#threadId.length > 0 && + (this.#evidence.providerExecutionKind === "remote_service" || + this.#evidence.providerPid !== null) + ) + return; + if (this.#handle?.child.exitCode !== null) + throw new Error("runnerd exited before provider startup"); await new Promise((resolveWait) => setTimeout(resolveWait, 10)); } throw new Error("runnerd did not report its provider identity"); @@ -893,19 +1327,25 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { while (Date.now() < deadline) { this.#throwIfFailed(); const command = this.#core?.store.state.commands.find((candidate) => - commandId === undefined ? candidate.type === type : candidate.commandId === commandId + commandId === undefined + ? candidate.type === type + : candidate.commandId === commandId, ); if (command?.status === "completed") return; if (command !== undefined && command.status !== "pending") { - throw new Error(`PRP command ${type} ${command.status}: ${JSON.stringify(command.result)}`); + throw new Error( + `PRP command ${type} ${command.status}: ${JSON.stringify(command.result)}`, + ); } - if (this.#handle?.child.exitCode !== null) throw new Error(`runnerd exited while waiting for ${type}`); + if (this.#handle?.child.exitCode !== null) + throw new Error(`runnerd exited while waiting for ${type}`); await new Promise((resolveWait) => setTimeout(resolveWait, 10)); } throw new Error(`PRP command ${type} timed out`); } #pumpEvents(): void { + this.#flushPendingTraceRehydrations(); const events = this.#core?.store.state.committedEvents ?? []; while (this.#eventIndex < events.length) { const event = events[this.#eventIndex++]; @@ -915,26 +1355,48 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { const descriptor = record(started.providerDescriptor); // Older durable snapshots serialized the enum fields in snake_case; // the provider descriptor is the canonical fallback during recovery. - const pid = runtimeIdentity.processId ?? runtimeIdentity.process_id ?? descriptor.processId ?? started.pid; + const pid = + runtimeIdentity.processId ?? + runtimeIdentity.process_id ?? + descriptor.processId ?? + started.pid; const threadId = started.threadId; const sessionId = started.sessionId; if (typeof pid === "number") { this.#evidence.providerPid = pid; - if (descriptor.driver === "acpx_runtime") this.#evidence.sidecarPid = pid; + if (descriptor.driver === "acpx_runtime") + this.#evidence.sidecarPid = pid; else this.#evidence.codexPid = pid; } - if (typeof descriptor.driver === "string") this.#evidence.providerDriver = descriptor.driver; - if (typeof descriptor.providerVersion === "string") this.#evidence.providerVersion = descriptor.providerVersion; - if (descriptor.agent === "pi" || descriptor.agent === "claude" || descriptor.agent === "codex") this.#evidence.acpxAgent = descriptor.agent; - if (typeof descriptor.agentServerVersion === "string") this.#evidence.agentServerVersion = descriptor.agentServerVersion; - if (typeof descriptor.agentRuntimeVersion === "string") this.#evidence.agentRuntimeVersion = descriptor.agentRuntimeVersion; - if (typeof descriptor.acpProtocolVersion === "number") this.#evidence.acpProtocolVersion = descriptor.acpProtocolVersion; - if (runtimeIdentity.executionKind === "local_process" || runtimeIdentity.executionKind === "remote_service") { + if (typeof descriptor.driver === "string") + this.#evidence.providerDriver = descriptor.driver; + if (typeof descriptor.providerVersion === "string") + this.#evidence.providerVersion = descriptor.providerVersion; + if ( + descriptor.agent === "pi" || + descriptor.agent === "claude" || + descriptor.agent === "codex" + ) + this.#evidence.acpxAgent = descriptor.agent; + if (typeof descriptor.agentServerVersion === "string") + this.#evidence.agentServerVersion = descriptor.agentServerVersion; + if (typeof descriptor.agentRuntimeVersion === "string") + this.#evidence.agentRuntimeVersion = descriptor.agentRuntimeVersion; + if (typeof descriptor.acpProtocolVersion === "number") + this.#evidence.acpProtocolVersion = descriptor.acpProtocolVersion; + if (typeof descriptor.agentProcessId === "number") + this.#evidence.agentPid = descriptor.agentProcessId; + if ( + runtimeIdentity.executionKind === "local_process" || + runtimeIdentity.executionKind === "remote_service" + ) { this.#evidence.providerExecutionKind = runtimeIdentity.executionKind; } if (runtimeIdentity.service === "anthropic_managed_agents") { this.#evidence.providerService = "anthropic_managed_agents"; - } else if (runtimeIdentity.service === "aws_bedrock_agentcore_harness") { + } else if ( + runtimeIdentity.service === "aws_bedrock_agentcore_harness" + ) { this.#evidence.providerService = "aws_bedrock_agentcore_harness"; } if (typeof threadId === "string") this.#threadId = threadId; @@ -944,61 +1406,148 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { } if (event.eventType === "harness.diagnostic") { const diagnostic = record(record(event.envelope.payload).payload); - if (diagnostic.providerMethod === "acpx/process" && diagnostic.role === "acp_agent" && typeof diagnostic.pid === "number") { + if ( + diagnostic.providerMethod === "acpx/process" && + diagnostic.role === "acp_agent" && + typeof diagnostic.pid === "number" + ) { this.#evidence.agentPid = diagnostic.pid; this.#publish(); } } const eventPayload = record(event.envelope.payload).payload; const sessionUpdatePayload = record(eventPayload); - const canonicalMethod = ({ - "turn.started": "turn/started", - "item.started": "item/started", - "item.delta": "item/agentMessage/delta", - "item.completed": "item/completed", - "turn.completed": "turn/completed", - "turn.failed": "turn/completed", - "turn.interrupted": "turn/completed", - "turn.cancelled": "turn/completed", - "usage.reported": "thread/tokenUsage/updated", - "session.updated": sessionUpdatePayload.status === "budget_reached" - ? "provider/budgetReached" - : "provider/sessionUpdated", - } as Record)[event.eventType]; - const notifications = event.eventType === "provider.event" - ? unwrapRunnerdProviderNotifications(eventPayload) - : canonicalMethod - ? [{ method: canonicalMethod, params: record(eventPayload) }] - : []; + const canonicalMethod = ( + { + "turn.started": "turn/started", + "item.started": "item/started", + "item.delta": "item/agentMessage/delta", + "item.completed": "item/completed", + "turn.completed": "turn/completed", + "turn.failed": "turn/completed", + "turn.interrupted": "turn/completed", + "turn.cancelled": "turn/completed", + "usage.reported": "thread/tokenUsage/updated", + "session.updated": + sessionUpdatePayload.status === "budget_reached" + ? "provider/budgetReached" + : "provider/sessionUpdated", + } as Record + )[event.eventType]; + const notifications = + event.eventType === "provider.event" + ? unwrapRunnerdProviderNotifications(eventPayload) + : canonicalMethod + ? [{ method: canonicalMethod, params: record(eventPayload) }] + : []; for (const payload of notifications) { const method = payload.method; if (typeof method !== "string") continue; const rawParams = record(payload.params); - const params = method === "thread/tokenUsage/updated" - ? rehydrateRunnerdUsageNotification(rawParams, this.#threadId, this.#turnId) - : method === "turn/started" || method === "turn/completed" - ? { ...rawParams, turnId: record(rawParams.turn).id ?? rawParams.turnId } - : rawParams; - if (params.turnId === undefined && typeof event.envelope.turnId === "string") { + const params = + method === "thread/tokenUsage/updated" + ? rehydrateRunnerdUsageNotification( + rawParams, + this.#threadId, + this.#turnId, + ) + : method === "turn/started" || method === "turn/completed" + ? { + ...rawParams, + turnId: record(rawParams.turn).id ?? rawParams.turnId, + } + : rawParams; + if ( + params.turnId === undefined && + typeof event.envelope.turnId === "string" + ) { params.turnId = event.envelope.turnId; } - const providerTurnId = method === "turn/started" || method === "turn/completed" - ? record(params.turn).id ?? params.turnId - : params.turnId ?? record(params.turn).id; + const providerTurnId = + method === "turn/started" || method === "turn/completed" + ? (record(params.turn).id ?? params.turnId) + : (params.turnId ?? record(params.turn).id); if (typeof providerTurnId === "string" && providerTurnId.length > 0) { this.#turnId = providerTurnId; } - this.#queue.push({ method, params }); + this.#queue.push({ + method, + params, + ...(this.options.environment?.PAPERCLIP_PROVIDER_TRACE_PATH + ? { + paperclipTrace: { + sourceEventId: event.sourceEventId, + sourceEventType: event.eventType, + }, + } + : {}), + }); + } + if (this.options.environment?.PAPERCLIP_PROVIDER_TRACE_PATH) { + const pending = { + sourceEventId: event.sourceEventId, + eventType: event.eventType, + visibleNotificationCount: notifications.length, + }; + const traceResult = appendRunnerdRehydrationTrace( + this.options.environment.PAPERCLIP_PROVIDER_TRACE_PATH, + pending.sourceEventId, + pending.eventType, + pending.visibleNotificationCount, + this.#nextTraceDebugSequence, + ); + if (traceResult === "written") { + this.#nextTraceDebugSequence += 1; + } else if ( + traceResult === "retry" && + this.#pendingTraceRehydrations.length < 4_096 + ) { + this.#pendingTraceRehydrations.push(pending); + } else if (traceResult === "retry") { + this.#traceRehydrationSpoolOverflow = true; + } } } } + #flushPendingTraceRehydrations(): void { + const tracePath = this.options.environment?.PAPERCLIP_PROVIDER_TRACE_PATH; + if (!tracePath) return; + const retry: PendingTraceRehydration[] = []; + for (const pending of this.#pendingTraceRehydrations) { + const traceResult = appendRunnerdRehydrationTrace( + tracePath, + pending.sourceEventId, + pending.eventType, + pending.visibleNotificationCount, + this.#nextTraceDebugSequence, + ); + if (traceResult === "written") this.#nextTraceDebugSequence += 1; + else if (traceResult === "retry") retry.push(pending); + } + this.#pendingTraceRehydrations = retry; + + const driverRetry: PendingDriverTraceInterpretation[] = []; + for (const pending of this.#pendingDriverTraceInterpretations) { + const traceResult = appendCodexDriverInterpretationTrace( + tracePath, + pending, + this.#nextTraceDebugSequence, + ); + if (traceResult === "written") this.#nextTraceDebugSequence += 1; + else if (traceResult === "retry") driverRetry.push(pending); + } + this.#pendingDriverTraceInterpretations = driverRetry; + } + #pumpEventsSafely(): void { try { this.#pumpEvents(); } catch (error) { this.#failTransport( - new Error(`provider_transport_failed: ${error instanceof Error ? error.message : String(error)}`), + new Error( + `provider_transport_failed: ${error instanceof Error ? error.message : String(error)}`, + ), ); } } @@ -1013,18 +1562,24 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { if (detail) this.#diagnostic(detail.slice(-4_096)); this.#publish(); if (this.#closed || this.#handle !== handle) return; - const code = /provider_frame_too_large|stdout frame exceeded/i.test(detail) + const code = /provider_frame_too_large|stdout frame exceeded/i.test( + detail, + ) ? "provider_frame_too_large" : "native_runner_process_exited"; - this.#failTransport(new Error( - `${code}: runnerd exited unexpectedly${result.code === null ? "" : ` with code ${result.code}`}${detail ? `: ${detail.slice(-1_000)}` : ""}`, - )); + this.#failTransport( + new Error( + `${code}: runnerd exited unexpectedly${result.code === null ? "" : ` with code ${result.code}`}${detail ? `: ${detail.slice(-1_000)}` : ""}`, + ), + ); }, (error) => { if (this.#closed || this.#handle !== handle) return; - this.#failTransport(new Error( - `native_runner_process_exited: runnerd process failed: ${error instanceof Error ? error.message : String(error)}`, - )); + this.#failTransport( + new Error( + `native_runner_process_exited: runnerd process failed: ${error instanceof Error ? error.message : String(error)}`, + ), + ); }, ); } @@ -1044,7 +1599,8 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { #diagnostic(message: string): void { this.#evidence.diagnostics.push(message); - if (this.#evidence.diagnostics.length > 64) this.#evidence.diagnostics.shift(); + if (this.#evidence.diagnostics.length > 64) + this.#evidence.diagnostics.shift(); this.options.onDiagnostic?.(message); this.#publish(); } @@ -1055,7 +1611,10 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { } export function defaultCapabilityRunnerdBinary(): string { - return resolve(packageRoot, `runner/target/debug/paperclip-runnerd${executableSuffix}`); + return resolve( + packageRoot, + `runner/target/debug/paperclip-runnerd${executableSuffix}`, + ); } /** Starts an authenticated durable PRP authority, runnerd, and Codex provider transport. */ @@ -1066,4 +1625,5 @@ export function createCapabilityRunnerdCodexTransport( return { transport, evidence: () => transport.evidence() }; } -export const createRunnerdCodexTransport = createCapabilityRunnerdCodexTransport; +export const createRunnerdCodexTransport = + createCapabilityRunnerdCodexTransport; diff --git a/packages/paperclip-runner/src/mock-core/durable-recovery.test.ts b/packages/paperclip-runner/src/mock-core/durable-recovery.test.ts index eaeb37d75fe..d98c58f4ba5 100644 --- a/packages/paperclip-runner/src/mock-core/durable-recovery.test.ts +++ b/packages/paperclip-runner/src/mock-core/durable-recovery.test.ts @@ -884,18 +884,71 @@ describe.sequential("Durable transport and recovery", () => { it("passes only the bootstrap capability and platform basics to the runner", () => { const environment = durableRecoveryInternals.runnerEnvironment("opaque-ticket", { PATH: "/usr/bin", + HOME: "/Users/tester", + AWS_PROFILE: "paperclip-dev", + AWS_REGION: "us-east-1", + AWS_ACCESS_KEY_ID: "must-not-cross-runner-boundary", + AWS_SESSION_TOKEN: "must-not-cross-runner-boundary", ANTHROPIC_API_KEY: "sk-ant-live-only", UNRELATED_SERVER_SECRET: "must-not-cross-runner-boundary", }); expect(environment.PAPERCLIP_RUNNER_BOOTSTRAP_TICKET).toBe("opaque-ticket"); expect(environment.ANTHROPIC_API_KEY).toBe("sk-ant-live-only"); + expect(environment.HOME).toBe("/Users/tester"); + expect(environment.AWS_PROFILE).toBe("paperclip-dev"); + expect(environment.AWS_REGION).toBe("us-east-1"); + expect(environment.AWS_ACCESS_KEY_ID).toBeUndefined(); + expect(environment.AWS_SESSION_TOKEN).toBeUndefined(); expect(environment.UNRELATED_SERVER_SECRET).toBeUndefined(); expect(environment.PAPERCLIP_API_KEY).toBeUndefined(); expect(environment.OPENAI_API_KEY).toBeUndefined(); expect(environment.EMAIL_AGENTMAIL_GENERAL_API_KEY).toBeUndefined(); }); + it("keeps the eval connection lease independent from the provider turn deadline", () => { + expect(durableRecoveryInternals.durableEvalConnectionLeaseTtlMs(180_000)) + .toBe(60 * 60 * 1_000); + expect(durableRecoveryInternals.durableEvalConnectionLeaseTtlMs(90 * 60 * 1_000)) + .toBe(91 * 60 * 1_000); + }); + + it("builds the pinned remote Claude Managed eval provider without local capabilities", () => { + const provider = durableRecoveryInternals.claudeManagedEvalProviderConfiguration( + "claude-sonnet-5", + { + profileId: "profile-qualified", + anthropicAgentId: "agent-qualified", + agentVersion: "17", + environmentId: "environment-qualified", + betaVersion: "managed-agents-2026-04-01", + maxSessionListCostUsd: 0.5, + }, + ); + expect(provider).toEqual({ + kind: "claude_managed", + model: "claude-sonnet-5", + profileId: "profile-qualified", + anthropicAgentId: "agent-qualified", + agentVersion: "17", + environmentId: "environment-qualified", + betaVersion: "managed-agents-2026-04-01", + maxSessionListCostUsd: 0.5, + }); + expect(provider).not.toHaveProperty("cwd"); + expect(provider).not.toHaveProperty("command"); + expect(provider).not.toHaveProperty("mcpServers"); + }); + + it("recognizes a managed provider budget stop before the generic turn timeout", () => { + expect(durableRecoveryInternals.managedProviderBudgetReached([{ + envelope: { payload: { eventType: "session.updated", payload: { status: "budget_reached" } } }, + }])).toBe(true); + expect(durableRecoveryInternals.managedProviderBudgetReached([{ + envelope: { payload: { eventType: "session.updated", payload: { status: "budget_increased" } } }, + }])).toBe(false); + }); + it("projects the authenticated result inside a discovered-capability gateway", () => { const inner = { ok: true, operationId: "list_agents", callId: "call-1", result: { actors: [] } }; expect(durableRecoveryInternals.projectedSemanticResult("invoke_discovered_capability", { diff --git a/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts b/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts index a49212e5acd..3414c6c15ee 100644 --- a/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts +++ b/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts @@ -399,6 +399,13 @@ export const providerDescriptorSchema = { ], "minimum": 1 }, + "agentProcessId": { + "type": [ + "integer", + "null" + ], + "minimum": 1 + }, "endpointArn": { "type": "string", "minLength": 1, @@ -564,7 +571,8 @@ export const providerDescriptorSchema = { "acpProtocolVersion", "agentServerPackage", "agentServerVersion", - "acpxRecordId" + "acpxRecordId", + "agentProcessId" ] } } diff --git a/packages/paperclip-runner/src/protocol/provider-descriptor.test.ts b/packages/paperclip-runner/src/protocol/provider-descriptor.test.ts index 8c0a878c6d7..c079b8a1527 100644 --- a/packages/paperclip-runner/src/protocol/provider-descriptor.test.ts +++ b/packages/paperclip-runner/src/protocol/provider-descriptor.test.ts @@ -6,6 +6,27 @@ import { providerDescriptorSchema } from "./generated/schema-bundle.js"; const validate = new Ajv2020({ allErrors: true, strict: false }).compile(providerDescriptorSchema); describe("provider runtime descriptor", () => { + it("accepts ACPX sidecar and child process identities separately", () => { + expect(validate({ + provider: "acpx", + driver: "acpx_runtime", + agent: "pi", + model: "openrouter/deepseek/deepseek-v4-flash-0731", + requestedModel: "openrouter/deepseek/deepseek-v4-flash-0731", + executionKind: "local_process", + providerVersion: "0.13.1", + providerSessionId: "pi-session-1", + processId: 41001, + agentProcessId: 41002, + acpProtocolVersion: 1, + agentServerPackage: "pi-acp", + agentServerVersion: "0.0.33", + agentRuntimePackage: "@earendil-works/pi-coding-agent", + agentRuntimeVersion: "0.84.2", + acpxRecordId: "acpx-record-1", + })).toBe(true); + }); + it("accepts the closed AWS AgentCore remote identity", () => { expect(validate({ provider: "aws_agentcore", diff --git a/packages/paperclip-runner/src/provider-events.test.ts b/packages/paperclip-runner/src/provider-events.test.ts index 2b73418fe6f..fc67468afa8 100644 --- a/packages/paperclip-runner/src/provider-events.test.ts +++ b/packages/paperclip-runner/src/provider-events.test.ts @@ -44,7 +44,7 @@ describe("provider-neutral events", () => { const cases: Array<[string, Record]> = [ ["item/completed", { item: { id: "plan-1", type: "plan", text: "Ship it" } }], ["item/completed", { item: { id: "exec-1", type: "commandExecution", status: "completed", output: "ok" } }], - ["item/completed", { item: { id: "web-1", type: "webSearch", action: { type: "search", query: "PRP" } } }], + ["item/completed", { item: { id: "web-1", type: "webSearch", action: { type: "search", query: "PRP" }, results: [{ ref_id: "source-1", title: "Protocol notes", url: "https://example.com/prp", snippet: "Canonical event details" }] } }], ["item/completed", { item: { id: "child-1", type: "collabAgentToolCall", tool: "spawnAgent" } }], ["model/rerouted", { turnId: "turn-1", fromModel: "gpt-5", toModel: "gpt-5.1", reason: "capacity" }], ["thread/compacted", { itemId: "compact-1" }], @@ -61,6 +61,27 @@ describe("provider-neutral events", () => { expect(mapped).toHaveLength(cases.length); for (const event of mapped) expect(validatePrpEvent(envelope(event))).toEqual({ ok: true, event: expect.any(Object), issues: [] }); expect(JSON.stringify(mapped.find((entry) => entry.eventType === "terminal.input.sent"))).not.toContain("secret input"); + expect(mapped.find((entry) => entry.eventType === "research.completed")?.payload.sources).toEqual([{ + sourceId: "source-1", + title: "Protocol notes", + url: "https://example.com/prp", + snippet: "Canonical event details", + }]); + }); + + it("bounds and filters Codex research sources before they enter PRP", () => { + const event = canonicalProviderEventsFromCodex("item/completed", { + item: { + id: "web-1", + type: "webSearch", + results: [ + { ref_id: "good", title: "Good", url: "https://example.com/good", snippet: "x".repeat(5000) }, + { ref_id: "unsafe", title: "Unsafe", url: "file:///etc/passwd" }, + ], + }, + })[0]!; + expect(event.payload.sources).toEqual([expect.objectContaining({ sourceId: "good", url: "https://example.com/good", snippet: "x".repeat(4000) })]); + expect(validatePrpEvent(envelope(event))).toEqual({ ok: true, event: expect.any(Object), issues: [] }); }); it("classifies only structured OpenCode parts and never assistant prose", () => { diff --git a/packages/paperclip-runner/src/provider-events.ts b/packages/paperclip-runner/src/provider-events.ts index f6866a9cc5a..8ef7bf01887 100644 --- a/packages/paperclip-runner/src/provider-events.ts +++ b/packages/paperclip-runner/src/provider-events.ts @@ -159,6 +159,22 @@ function planPayload(params: Record, item: Record, researchId: string): Array> { + if (!Array.isArray(item.results)) return []; + return item.results.slice(0, 64).flatMap((value, index) => { + const result = record(value); + const url = text(result.url).slice(0, 8192); + if (!url.startsWith("http://") && !url.startsWith("https://")) return []; + const fallbackId = `${researchId}:source:${index + 1}`; + return [{ + sourceId: safeId(text(result.ref_id, text(result.refId, fallbackId)), fallbackId), + title: text(result.title, url).slice(0, 4000), + url, + snippet: text(result.snippet).slice(0, 4000) || null, + }]; + }); +} + /** Maps qualified Codex app-server notifications to bounded canonical PRP events. */ export function canonicalProviderEventsFromCodex(method: string, paramsValue: unknown): CanonicalProviderEvent[] { const params = record(paramsValue); @@ -183,7 +199,7 @@ export function canonicalProviderEventsFromCodex(method: string, paramsValue: un const action = record(item.action); const actionType = text(action.type, "other").replace("openPage", "open_page").replace("findInPage", "find_in_page"); const url = text(action.url) || null; - return [{ eventType: completed ? "research.completed" : "research.started", payload: { schema: "paperclip.research.v1", researchId: itemId, action: actionType, status: completed ? "completed" : "running", query: text(item.query, text(action.query)) || null, url: url?.startsWith("http://") || url?.startsWith("https://") ? url : null, pattern: text(action.pattern) || null, sources: [] }, itemId }]; + return [{ eventType: completed ? "research.completed" : "research.started", payload: { schema: "paperclip.research.v1", researchId: itemId, action: actionType, status: completed ? "completed" : "running", query: text(item.query, text(action.query)) || null, url: url?.startsWith("http://") || url?.startsWith("https://") ? url : null, pattern: text(action.pattern) || null, sources: completed ? researchSources(item, itemId) : [] }, itemId }]; } if ((method === "item/started" || completed) && (type === "collabAgentToolCall" || type === "subAgentActivity")) { return [{ eventType: completed ? "delegation.completed" : "delegation.started", payload: { schema: "paperclip.delegation.v1", delegationId: itemId, action: text(item.tool, "spawnAgent").replace("spawnAgent", "spawn").replace("sendInput", "message").replace("resumeAgent", "resume").replace("closeAgent", "close"), status: completed ? "completed" : "running", children: [] }, itemId }]; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 22780fd1146..f145f3b1353 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,7 @@ -export { agentAdapterTypeSchema, optionalAgentAdapterTypeSchema } from "./adapter-type.js"; +export { + agentAdapterTypeSchema, + optionalAgentAdapterTypeSchema, +} from "./adapter-type.js"; export { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./adapter-auth-check-code.js"; export { nativeFinalizationResultSchema, @@ -258,7 +261,11 @@ export { } from "./app-definitions.js"; export { APP_DEFINITIONS } from "./app-definitions.generated.js"; export * from "./validators/status-card.js"; -export { appDefinitionSchema, appDefinitionsSchema, connectionMethodDefSchema } from "./validators/app-definition.js"; +export { + appDefinitionSchema, + appDefinitionsSchema, + connectionMethodDefSchema, +} from "./validators/app-definition.js"; export { humanizeConnectionDisplayName, connectionDisplaySecondaryHint, @@ -1139,6 +1146,17 @@ export type { HeartbeatRun, HeartbeatRunEvent, HeartbeatRunStatusPhase, + ProviderTraceDebugRequest, + ProviderTraceDirection, + ProviderTraceDisposition, + ProviderTraceFieldMapping, + ProviderTraceFieldMappingAction, + ProviderTraceFrame, + ProviderTraceInterpretation, + ProviderTraceMetadata, + ProviderTraceStatus, + RunPresentationDecision, + RunPresentationSource, AgentRuntimeState, AgentTaskSession, AgentWakeupRequest, @@ -1621,7 +1639,11 @@ export { SMOKE_RUN_TRIGGERS, } from "./types/smoke-lab.js"; -export type { ServerGitInfo, ServerGitLocalChanges, ServerInfoSnapshot } from "./types/server-info.js"; +export type { + ServerGitInfo, + ServerGitLocalChanges, + ServerInfoSnapshot, +} from "./types/server-info.js"; export { getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, @@ -2313,8 +2335,16 @@ export { } from "./validators/index.js"; export { API_PREFIX, API } from "./api.js"; -export { normalizeAgentUrlKey, deriveAgentUrlKey, isUuidLike } from "./agent-url-key.js"; -export { deriveProjectUrlKey, normalizeProjectUrlKey, hasNonAsciiContent } from "./project-url-key.js"; +export { + normalizeAgentUrlKey, + deriveAgentUrlKey, + isUuidLike, +} from "./agent-url-key.js"; +export { + deriveProjectUrlKey, + normalizeProjectUrlKey, + hasNonAsciiContent, +} from "./project-url-key.js"; export { AGENT_MENTION_SCHEME, PROJECT_MENTION_SCHEME, @@ -2476,6 +2506,7 @@ export { type EnvironmentCustomImageTerminalSessionToken, } from "./validators/environment-custom-images.js"; export * from "./validators/skill-policy.js"; +export * from "./validators/provider-trace.js"; export { FEATURE_TIERS, INSTANCE_FEATURE_CATALOG, diff --git a/packages/shared/src/types/heartbeat.ts b/packages/shared/src/types/heartbeat.ts index 1c2592edd06..cf89d5fbc4b 100644 --- a/packages/shared/src/types/heartbeat.ts +++ b/packages/shared/src/types/heartbeat.ts @@ -8,9 +8,103 @@ import type { WakeupRequestStatus, } from "../constants.js"; -export type GitWorktreeBranchAncestryVerdict = "ancestor" | "diverged" | "unknown"; +export interface ProviderTraceDebugRequest { + providerTrace: "raw"; +} -export type GitWorktreeInProgressOperation = "rebase" | "merge" | "cherry_pick" | "revert" | "bisect"; +export type ProviderTraceDirection = + "client_to_provider" | "provider_to_client" | "provider_stderr"; +export type ProviderTraceDisposition = + "mapped" | "generic" | "ignored" | "rejected" | "operator_only"; + +export type ProviderTraceFieldMappingAction = + | "copied" + | "renamed" + | "normalized" + | "derived" + | "dropped" + | "redacted"; + +export interface ProviderTraceFieldMapping { + inputPath?: string; + outputPath?: string; + action: ProviderTraceFieldMappingAction; + reason?: string; +} + +export interface ProviderTraceFrame { + kind?: "frame"; + schema: "paperclip.provider_trace_frame.v1"; + debugChannel: string; + debugSequence: number; + frameId: number; + timestamp: string; + direction: ProviderTraceDirection; + transport: string; + provider: string; + byteLength: number; + digest: `sha256:${string}`; + rawBase64: string; +} + +export interface ProviderTraceInterpretation { + kind?: "interpretation"; + schema: "paperclip.provider_trace_interpretation.v1"; + debugChannel: string; + debugSequence: number; + frameId: number; + stage: string; + ruleId: string; + disposition: ProviderTraceDisposition; + emittedEventIds: string[]; + droppedFields: string[]; + fieldMappings?: ProviderTraceFieldMapping[]; + reason: string; +} + +export type ProviderTraceStatus = + "capturing" | "complete" | "incomplete" | "truncated" | "deleted" | "expired"; + +export interface ProviderTraceMetadata { + schema: "paperclip.provider_trace_metadata.v1"; + id: string; + runId: string; + companyId: string; + status: ProviderTraceStatus; + provider: string; + frameCount: number; + byteCount: number; + digest: `sha256:${string}` | null; + reason: string | null; + requestedBy: string; + createdAt: string | Date; + expiresAt: string | Date; + deletedAt: string | Date | null; +} + +export type RunPresentationSource = + | "existing_issue_comment" + | "final_agent_message" + | "semantic_result_summary" + | "adapter_final_response" + | "none"; + +export interface RunPresentationDecision { + schema: "paperclip.run_presentation_decision.v1"; + resolverVersion: string; + chosenSource: RunPresentationSource; + sourceEventId: string | null; + commentAction: "reuse" | "create" | "none"; + commentId: string | null; + activityDisposition: "collapse"; + reasonCodes: string[]; +} + +export type GitWorktreeBranchAncestryVerdict = + "ancestor" | "diverged" | "unknown"; + +export type GitWorktreeInProgressOperation = + "rebase" | "merge" | "cherry_pick" | "revert" | "bisect"; export interface GitWorktreeBranchIncoherenceEvidence { reason: "git_worktree_branch_incoherence"; @@ -141,11 +235,7 @@ export type HeartbeatRunStatusPhase = | "run_activity"; export type HeartbeatRunOutputSilenceLevel = - | "not_applicable" - | "ok" - | "suspicious" - | "critical" - | "snoozed"; + "not_applicable" | "ok" | "suspicious" | "critical" | "snoozed"; export interface HeartbeatRunOutputSilence { lastOutputAt: Date | string | null; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index f49638af1c2..d4bfff78bb2 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -341,7 +341,15 @@ export type { DocumentTextRange, UpdateDocumentAnnotationThreadRequest, } from "./document-annotation.js"; -export type { Project, ProjectBudgetSummary, ProjectCodebase, ProjectCodebaseOrigin, ProjectGoalRef, ProjectManagedByPlugin, ProjectWorkspace } from "./project.js"; +export type { + Project, + ProjectBudgetSummary, + ProjectCodebase, + ProjectCodebaseOrigin, + ProjectGoalRef, + ProjectManagedByPlugin, + ProjectWorkspace, +} from "./project.js"; export type { CompanySearchCountType, CompanySearchExtractIssueResult, @@ -832,8 +840,23 @@ export type { RoutineExecutionIssueOrigin, RoutineListItem, } from "./routine.js"; -export type { CostEvent, CostSummary, IssueCostSummary, CostByAgent, CostByProviderModel, CostByBiller, CostByAgentModel, CostWindowSpendRow, CostByProject } from "./cost.js"; -export type { FinanceEvent, FinanceSummary, FinanceByBiller, FinanceByKind } from "./finance.js"; +export type { + CostEvent, + CostSummary, + IssueCostSummary, + CostByAgent, + CostByProviderModel, + CostByBiller, + CostByAgentModel, + CostWindowSpendRow, + CostByProject, +} from "./cost.js"; +export type { + FinanceEvent, + FinanceSummary, + FinanceByBiller, + FinanceByKind, +} from "./finance.js"; export type { AgentWakeupResponse, AgentWakeupSkipped, @@ -843,6 +866,17 @@ export type { HeartbeatRun, HeartbeatRunEvent, HeartbeatRunStatusPhase, + ProviderTraceDebugRequest, + ProviderTraceDirection, + ProviderTraceDisposition, + ProviderTraceFieldMapping, + ProviderTraceFieldMappingAction, + ProviderTraceFrame, + ProviderTraceInterpretation, + ProviderTraceMetadata, + ProviderTraceStatus, + RunPresentationDecision, + RunPresentationSource, AgentRuntimeState, AgentTaskSession, AgentWakeupRequest, diff --git a/packages/shared/src/validators/agent.ts b/packages/shared/src/validators/agent.ts index 308cd057c15..0d62412c065 100644 --- a/packages/shared/src/validators/agent.ts +++ b/packages/shared/src/validators/agent.ts @@ -59,13 +59,19 @@ export const createAgentInstructionsBundleSchema = z.object({ const agentModelProfileConfigSchema = z.object({ enabled: z.boolean().optional(), label: z.string().trim().min(1).optional(), - adapterConfig: adapterConfigSchema, + // Disabled profiles created before model-profile editing may not have an + // adapter payload yet. Keep them valid so unrelated runtime settings (such + // as debug capture) can be updated without fabricating model configuration. + adapterConfig: adapterConfigSchema.optional().default({}), }).strict(); export const agentRuntimeConfigSchema = z.object({ modelProfiles: z.object({ cheap: agentModelProfileConfigSchema.optional(), }).strict().optional(), + debug: z.object({ + providerTrace: z.literal("raw").optional(), + }).strict().optional(), }).catchall(z.unknown()); export const createAgentSchema = z.object({ @@ -208,6 +214,9 @@ export const wakeAgentSchema = z.object({ (value) => (value === null ? undefined : value), z.boolean().optional().default(false), ), + debug: z.object({ + providerTrace: z.literal("raw"), + }).strict().optional(), }); export type WakeAgent = z.infer; diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index a7a110ad762..487a4fc82e6 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -941,4 +941,5 @@ export { type RevokeToolTrustRule, } from "./tool-access.js"; export * from "./skill-policy.js"; +export * from "./provider-trace.js"; export * from "./app-definition.js"; diff --git a/packages/shared/src/validators/provider-trace.ts b/packages/shared/src/validators/provider-trace.ts new file mode 100644 index 00000000000..b38b6200a2e --- /dev/null +++ b/packages/shared/src/validators/provider-trace.ts @@ -0,0 +1,117 @@ +import { z } from "zod"; + +export const providerTraceDirectionSchema = z.enum([ + "client_to_provider", + "provider_to_client", + "provider_stderr", +]); + +export const providerTraceDispositionSchema = z.enum([ + "mapped", + "generic", + "ignored", + "rejected", + "operator_only", +]); + +export const providerTraceFieldMappingSchema = z + .object({ + inputPath: z.string().min(1).optional(), + outputPath: z.string().min(1).optional(), + action: z.enum([ + "copied", + "renamed", + "normalized", + "derived", + "dropped", + "redacted", + ]), + reason: z.string().optional(), + }) + .strict(); + +export const providerTraceStatusSchema = z.enum([ + "capturing", + "complete", + "incomplete", + "truncated", + "deleted", + "expired", +]); + +const sha256DigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/i); +const dateValueSchema = z.union([z.string().datetime(), z.date()]); + +export const providerTraceFrameSchema = z + .object({ + kind: z.literal("frame").optional(), + schema: z.literal("paperclip.provider_trace_frame.v1"), + debugChannel: z.string().min(1), + debugSequence: z.number().int().positive(), + frameId: z.number().int().positive(), + timestamp: z.string().min(1), + direction: providerTraceDirectionSchema, + transport: z.string().min(1), + provider: z.string().min(1), + byteLength: z.number().int().nonnegative(), + digest: sha256DigestSchema, + rawBase64: z.string(), + }) + .strict(); + +export const providerTraceInterpretationSchema = z + .object({ + kind: z.literal("interpretation").optional(), + schema: z.literal("paperclip.provider_trace_interpretation.v1"), + debugChannel: z.string().min(1), + debugSequence: z.number().int().positive(), + frameId: z.number().int().positive(), + stage: z.string().min(1), + ruleId: z.string().min(1), + disposition: providerTraceDispositionSchema, + emittedEventIds: z.array(z.string()), + droppedFields: z.array(z.string()), + fieldMappings: z.array(providerTraceFieldMappingSchema).optional(), + reason: z.string(), + }) + .strict(); + +export const providerTraceMetadataSchema = z + .object({ + schema: z.literal("paperclip.provider_trace_metadata.v1"), + id: z.string().min(1), + runId: z.string().min(1), + companyId: z.string().min(1), + status: providerTraceStatusSchema, + provider: z.string().min(1), + frameCount: z.number().int().nonnegative(), + byteCount: z.number().int().nonnegative(), + digest: sha256DigestSchema.nullable(), + reason: z.string().nullable(), + requestedBy: z.string().min(1), + createdAt: dateValueSchema, + expiresAt: dateValueSchema, + deletedAt: dateValueSchema.nullable(), + }) + .strict(); + +export const runPresentationSourceSchema = z.enum([ + "existing_issue_comment", + "final_agent_message", + "semantic_result_summary", + "adapter_final_response", + "none", +]); + +export const runPresentationDecisionSchema = z + .object({ + schema: z.literal("paperclip.run_presentation_decision.v1"), + resolverVersion: z.string().min(1), + chosenSource: runPresentationSourceSchema, + sourceEventId: z.string().nullable(), + commentAction: z.enum(["reuse", "create", "none"]), + commentId: z.string().nullable(), + activityDisposition: z.literal("collapse"), + reasonCodes: z.array(z.string()), + }) + .strict(); diff --git a/patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch b/patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch new file mode 100644 index 00000000000..8781ff35269 --- /dev/null +++ b/patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch @@ -0,0 +1,27 @@ +diff --git a/dist/acp-agent.js b/dist/acp-agent.js +--- a/dist/acp-agent.js ++++ b/dist/acp-agent.js +@@ -2681,10 +2681,20 @@ + cost: { + amount: message.total_cost_usd, + currency: "USD", + }, +- ...(message.origin && { +- _meta: { "_claude/origin": message.origin }, +- }), ++ _meta: { ++ ...(message.origin && { "_claude/origin": message.origin }), ++ // ACP's core Usage shape exposes context occupancy and ++ // cost, but not the billable input/output split. Keep the ++ // SDK's bounded aggregate in extension metadata so ACPX can ++ // normalize it without persisting a raw provider message. ++ usage: { ++ input_tokens: message.usage.input_tokens, ++ output_tokens: message.usage.output_tokens, ++ cache_read_input_tokens: message.usage.cache_read_input_tokens, ++ cache_creation_input_tokens: message.usage.cache_creation_input_tokens, ++ }, ++ }, + }, + }); + } diff --git a/patches/pi-acp@0.0.33.patch b/patches/pi-acp@0.0.33.patch index 6e28395eb24..73b76e683ef 100644 --- a/patches/pi-acp@0.0.33.patch +++ b/patches/pi-acp@0.0.33.patch @@ -27,7 +27,7 @@ index ead9809cabc9f3b203f1769db3cabb8327dd758a..113b166120711705edcabaca2d63ae40 if (params.sessionPath) args.push("--session", params.sessionPath); const child = spawn(cmd, args, { cwd: params.cwd, -@@ -949,6 +958,26 @@ var PiAcpSession = class { +@@ -949,6 +958,22 @@ var PiAcpSession = class { switch (type) { case "message_update": { const ame = ev.assistantMessageEvent; @@ -36,19 +36,15 @@ index ead9809cabc9f3b203f1769db3cabb8327dd758a..113b166120711705edcabaca2d63ae40 + const fingerprint = JSON.stringify(usage); + if (fingerprint !== this.paperclipUsageFingerprint && (usage.totalTokens ?? 0) > 0) { + this.paperclipUsageFingerprint = fingerprint; -+ this.emit({ -+ sessionUpdate: "usage_update", -+ used: typeof usage.totalTokens === "number" ? usage.totalTokens : void 0, -+ cost: typeof usage.cost?.total === "number" ? { amount: usage.cost.total, currency: "USD" } : void 0, -+ _meta: { usage: { ++ process.stderr.write(`[paperclip-pi-usage-v1]${JSON.stringify({ + inputTokens: typeof usage.input === "number" ? usage.input : void 0, + outputTokens: typeof usage.output === "number" ? usage.output : void 0, + cachedReadTokens: typeof usage.cacheRead === "number" ? usage.cacheRead : void 0, + cachedWriteTokens: typeof usage.cacheWrite === "number" ? usage.cacheWrite : void 0, + thoughtTokens: typeof usage.reasoning === "number" ? usage.reasoning : void 0, -+ totalTokens: typeof usage.totalTokens === "number" ? usage.totalTokens : void 0 -+ } } -+ }); ++ totalTokens: typeof usage.totalTokens === "number" ? usage.totalTokens : void 0, ++ cost: typeof usage.cost?.total === "number" ? { amount: usage.cost.total, currency: "USD" } : void 0 ++ })}\n`); + } + } if (ame?.type === "text_delta" && typeof ame.delta === "string") { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6ea06bb03e..cd5d5a52bff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ overrides: '@lexical/utils': 0.49.0 patchedDependencies: + '@agentclientprotocol/claude-agent-acp@0.70.0': + hash: 3uuiqvasvwjam3tge5jo3jbw7a + path: patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch acpx@0.13.1: hash: c5ivrcgwkvhn7lv7slpppkd7sy path: patches/acpx@0.13.1.patch @@ -27,7 +30,7 @@ patchedDependencies: hash: 55uhvnotpqyiy37rn3pqpukhei path: patches/embedded-postgres@18.1.0-beta.16.patch pi-acp@0.0.33: - hash: nwy4ztc7fl2g2mu436sxq5sba4 + hash: v6qgcxfo3rn3zt3m56ysulht2y path: patches/pi-acp@0.0.33.patch importers: @@ -446,7 +449,7 @@ importers: dependencies: '@agentclientprotocol/claude-agent-acp': specifier: 0.70.0 - version: 0.70.0(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@modelcontextprotocol/sdk@1.30.0(zod@3.25.76)) + version: 0.70.0(patch_hash=3uuiqvasvwjam3tge5jo3jbw7a)(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@modelcontextprotocol/sdk@1.30.0(zod@3.25.76)) '@agentclientprotocol/codex-acp': specifier: 1.6.2 version: 1.6.2 @@ -461,7 +464,7 @@ importers: version: 8.20.0 pi-acp: specifier: 0.0.33 - version: 0.0.33(patch_hash=nwy4ztc7fl2g2mu436sxq5sba4) + version: 0.0.33(patch_hash=v6qgcxfo3rn3zt3m56ysulht2y) devDependencies: '@playwright/test': specifier: ^1.61.1 @@ -9340,7 +9343,7 @@ snapshots: - '@anthropic-ai/sdk' - '@modelcontextprotocol/sdk' - '@agentclientprotocol/claude-agent-acp@0.70.0(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@modelcontextprotocol/sdk@1.30.0(zod@3.25.76))': + '@agentclientprotocol/claude-agent-acp@0.70.0(patch_hash=3uuiqvasvwjam3tge5jo3jbw7a)(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@modelcontextprotocol/sdk@1.30.0(zod@3.25.76))': dependencies: '@agentclientprotocol/sdk': 1.3.0(zod@3.25.76) '@anthropic-ai/claude-agent-sdk': 0.3.232(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@modelcontextprotocol/sdk@1.30.0(zod@3.25.76))(zod@3.25.76) @@ -16781,7 +16784,7 @@ snapshots: dependencies: split2: 4.2.0 - pi-acp@0.0.33(patch_hash=nwy4ztc7fl2g2mu436sxq5sba4): + pi-acp@0.0.33(patch_hash=v6qgcxfo3rn3zt3m56ysulht2y): dependencies: '@agentclientprotocol/sdk': 0.26.0(zod@3.25.76) zod: 3.25.76 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5bc8038cf95..846ebb75513 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,3 +12,9 @@ packages: - server - ui - cli + +patchedDependencies: + embedded-postgres@18.1.0-beta.16: patches/embedded-postgres@18.1.0-beta.16.patch + acpx@0.13.1: patches/acpx@0.13.1.patch + pi-acp@0.0.33: patches/pi-acp@0.0.33.patch + '@agentclientprotocol/claude-agent-acp@0.70.0': patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch diff --git a/scripts/acpx-patch-packaging.test.mjs b/scripts/acpx-patch-packaging.test.mjs index 8f0df271440..fa672944b42 100644 --- a/scripts/acpx-patch-packaging.test.mjs +++ b/scripts/acpx-patch-packaging.test.mjs @@ -30,6 +30,10 @@ const runnerPackage = JSON.parse( ); const acpxRuntimePatch = await readFile(new URL("../patches/acpx@0.13.1.patch", import.meta.url), "utf8"); const piAcpPatch = await readFile(new URL("../patches/pi-acp@0.0.33.patch", import.meta.url), "utf8"); +const claudeAcpPatch = await readFile( + new URL("../patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch", import.meta.url), + "utf8", +); const dbPackage = JSON.parse( await readFile(new URL("../packages/db/package.json", import.meta.url), "utf8"), ); @@ -51,6 +55,10 @@ test("published packages preserve the patched ACPX runtime", () => { test("Paperclip Runner pins the qualified ACPX host and Pi isolation callbacks", () => { assert.equal(rootPackage.pnpm.patchedDependencies["acpx@0.13.1"], "patches/acpx@0.13.1.patch"); assert.equal(rootPackage.pnpm.patchedDependencies["pi-acp@0.0.33"], "patches/pi-acp@0.0.33.patch"); + assert.equal( + rootPackage.pnpm.patchedDependencies["@agentclientprotocol/claude-agent-acp@0.70.0"], + "patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch", + ); assert.equal(runnerPackage.dependencies.acpx, "0.13.1"); assert.equal(runnerPackage.dependencies["pi-acp"], "0.0.33"); assert.equal(runnerPackage.dependencies["@earendil-works/pi-coding-agent"], "0.84.2"); @@ -61,7 +69,9 @@ test("Paperclip Runner pins the qualified ACPX host and Pi isolation callbacks", "onSessionNotification", "onClientOperation", ]) assert.match(acpxRuntimePatch, new RegExp(callback)); assert.match(piAcpPatch, /PI_ACP_PI_ARGS_JSON/); - assert.match(piAcpPatch, /sessionUpdate: "usage_update"/); + assert.match(piAcpPatch, /\[paperclip-pi-usage-v1\]/); + assert.match(claudeAcpPatch, /usage: \{/); + assert.match(claudeAcpPatch, /cache_creation_input_tokens/); }); test("published packages preserve the patched embedded-postgres runtime", () => { diff --git a/server/src/__tests__/agent-live-run-routes.test.ts b/server/src/__tests__/agent-live-run-routes.test.ts index b9d376e9ac1..ed7d6e8f910 100644 --- a/server/src/__tests__/agent-live-run-routes.test.ts +++ b/server/src/__tests__/agent-live-run-routes.test.ts @@ -14,6 +14,7 @@ const mockHeartbeatService = vi.hoisted(() => ({ getRunLogAccess: vi.fn(), readLog: vi.fn(), wakeup: vi.fn(), + getRun: vi.fn(), })); const mockIssueService = vi.hoisted(() => ({ @@ -29,13 +30,26 @@ const mockInstanceSettingsService = vi.hoisted(() => ({ })); const mockRunSecretRedactionRegistry = vi.hoisted(() => ({ - redactForRun: vi.fn(async (_companyId: string, _runId: string, value: unknown) => value), + redactForRun: vi.fn( + async (_companyId: string, _runId: string, value: unknown) => value, + ), })); +const mockProviderTraceStore = vi.hoisted(() => ({ + inspect: vi.fn(), + revealFrame: vi.fn(), + download: vi.fn(), + remove: vi.fn(), + listMetadataForRuns: vi.fn(), +})); +const mockLogActivity = vi.hoisted(() => vi.fn()); + const routeAgentId = "11111111-1111-4111-8111-111111111111"; function registerModuleMocks() { - vi.doMock("../routes/authz.js", async () => vi.importActual("../routes/authz.js")); + vi.doMock("../routes/authz.js", async () => + vi.importActual("../routes/authz.js"), + ); vi.doMock("../services/agents.js", () => ({ agentService: () => mockAgentService, @@ -57,6 +71,10 @@ function registerModuleMocks() { createRunSecretRedactionRegistry: () => mockRunSecretRedactionRegistry, })); + vi.doMock("../services/provider-trace-store.js", () => ({ + providerTraceStore: () => mockProviderTraceStore, + })); + vi.doMock("../services/index.js", () => ({ agentService: () => mockAgentService, agentInstructionsService: () => ({}), @@ -77,7 +95,7 @@ function registerModuleMocks() { heartbeatService: () => mockHeartbeatService, issueApprovalService: () => ({}), issueService: () => mockIssueService, - logActivity: vi.fn(), + logActivity: mockLogActivity, secretService: () => ({}), syncInstructionsBundleConfigFromFilePath: vi.fn((_agent, config) => config), workspaceOperationService: () => ({}), @@ -92,21 +110,28 @@ function registerModuleMocks() { })); } -async function createApp(db: Record = {}) { +async function createApp( + db: Record = {}, + actor: Record = { + type: "board", + userId: "local-board", + companyIds: ["company-1"], + source: "local_implicit", + isInstanceAdmin: false, + }, +) { const [{ agentRoutes }, { errorHandler }] = await Promise.all([ - vi.importActual("../routes/agents.js"), - vi.importActual("../middleware/index.js"), + vi.importActual( + "../routes/agents.js", + ), + vi.importActual( + "../middleware/index.js", + ), ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { - (req as any).actor = { - type: "board", - userId: "local-board", - companyIds: ["company-1"], - source: "local_implicit", - isInstanceAdmin: false, - }; + (req as any).actor = actor; next(); }); app.use("/api", agentRoutes(db as any)); @@ -118,7 +143,8 @@ function createLiveRunsDbStub(rows: Array>) { const limit = vi.fn(async (value: number) => rows.slice(0, value)); const orderedQuery = { limit, - then: (resolve: (value: Array>) => unknown) => Promise.resolve(rows).then(resolve), + then: (resolve: (value: Array>) => unknown) => + Promise.resolve(rows).then(resolve), }; const query = { from: vi.fn().mockReturnThis(), @@ -139,7 +165,8 @@ async function requestApp( app: express.Express, buildRequest: (baseUrl: string) => request.Test, ) { - const { createServer } = await vi.importActual("node:http"); + const { createServer } = + await vi.importActual("node:http"); const server = createServer(app); try { await new Promise((resolve) => { @@ -222,7 +249,9 @@ describe("agent live run routes", () => { agentId: "agent-1", issueId: "issue-1", }); - mockHeartbeatService.getActiveRunIssueSummaryForAgent.mockResolvedValue(null); + mockHeartbeatService.getActiveRunIssueSummaryForAgent.mockResolvedValue( + null, + ); mockHeartbeatService.buildRunOutputSilence.mockResolvedValue(null); mockHeartbeatService.getRunLogAccess.mockResolvedValue({ id: "run-1", @@ -245,17 +274,28 @@ describe("agent live run routes", () => { invocationSource: "on_demand", triggerDetail: "manual", }); + mockHeartbeatService.getRun.mockResolvedValue({ + id: "run-1", + companyId: "company-1", + agentId: "agent-1", + status: "succeeded", + }); + mockProviderTraceStore.inspect.mockResolvedValue({ + trace: null, + entries: [], + }); }); it("returns a compact active run payload for issue polling", async () => { - const res = await requestApp( - await createApp(), - (baseUrl) => request(baseUrl).get("/api/issues/pc1a2-1295/active-run"), + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl).get("/api/issues/pc1a2-1295/active-run"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PC1A2-1295"); - expect(mockHeartbeatService.getRunIssueSummary).toHaveBeenCalledWith("run-1"); + expect(mockHeartbeatService.getRunIssueSummary).toHaveBeenCalledWith( + "run-1", + ); expect(res.body).toMatchObject({ id: "run-1", status: "running", @@ -303,14 +343,17 @@ describe("agent live run routes", () => { issueId: "issue-1", }); - const res = await requestApp( - await createApp(), - (baseUrl) => request(baseUrl).get("/api/issues/PC1A2-1295/active-run"), + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl).get("/api/issues/PC1A2-1295/active-run"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); - expect(mockHeartbeatService.getRunIssueSummary).toHaveBeenCalledWith("run-1"); - expect(mockHeartbeatService.getActiveRunIssueSummaryForAgent).toHaveBeenCalledWith("agent-1"); + expect(mockHeartbeatService.getRunIssueSummary).toHaveBeenCalledWith( + "run-1", + ); + expect( + mockHeartbeatService.getActiveRunIssueSummaryForAgent, + ).toHaveBeenCalledWith("agent-1"); expect(res.body).toMatchObject({ id: "run-1", issueId: "issue-1", @@ -330,9 +373,8 @@ describe("agent live run routes", () => { lastEventAt: new Date("2026-04-10T09:30:06.000Z"), })); - const res = await requestApp( - await createApp(), - (baseUrl) => request(baseUrl).get("/api/issues/PC1A2-1295/active-run"), + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl).get("/api/issues/PC1A2-1295/active-run"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); @@ -350,22 +392,26 @@ describe("agent live run routes", () => { }); it("uses narrow run log metadata lookups for log polling", async () => { - const res = await requestApp( - await createApp(), - (baseUrl) => request(baseUrl).get("/api/heartbeat-runs/run-1/log?offset=12&limitBytes=64"), + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl).get( + "/api/heartbeat-runs/run-1/log?offset=12&limitBytes=64", + ), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(mockHeartbeatService.getRunLogAccess).toHaveBeenCalledWith("run-1"); - expect(mockHeartbeatService.readLog).toHaveBeenCalledWith({ - id: "run-1", - companyId: "company-1", - logStore: "local_file", - logRef: "logs/run-1.ndjson", - }, { - offset: 12, - limitBytes: 64, - }); + expect(mockHeartbeatService.readLog).toHaveBeenCalledWith( + { + id: "run-1", + companyId: "company-1", + logStore: "local_file", + logRef: "logs/run-1.ndjson", + }, + { + offset: 12, + limitBytes: 64, + }, + ); expect(res.body).toEqual({ runId: "run-1", store: "local_file", @@ -384,7 +430,9 @@ describe("agent live run routes", () => { triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, - createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), + createdAt: new Date( + `2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`, + ), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", @@ -403,15 +451,16 @@ describe("agent live run routes", () => { })); const { db, limit } = createLiveRunsDbStub(rows); - const res = await requestApp( - await createApp(db), - (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs"), + const res = await requestApp(await createApp(db), (baseUrl) => + request(baseUrl).get("/api/companies/company-1/live-runs"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(limit).toHaveBeenCalledWith(50); expect(res.body).toHaveLength(50); - expect(mockHeartbeatService.buildRunOutputSilence).toHaveBeenCalledTimes(50); + expect(mockHeartbeatService.buildRunOutputSilence).toHaveBeenCalledTimes( + 50, + ); }); it("treats explicit zero or invalid live run limit as the capped default", async () => { @@ -423,7 +472,9 @@ describe("agent live run routes", () => { triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, - createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), + createdAt: new Date( + `2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`, + ), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", @@ -442,9 +493,10 @@ describe("agent live run routes", () => { })); const { db, limit } = createLiveRunsDbStub(rows); - const res = await requestApp( - await createApp(db), - (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs?limit=0&minCount=0"), + const res = await requestApp(await createApp(db), (baseUrl) => + request(baseUrl).get( + "/api/companies/company-1/live-runs?limit=0&minCount=0", + ), ); expect(res.status, JSON.stringify(res.body)).toBe(200); @@ -461,7 +513,9 @@ describe("agent live run routes", () => { triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, - createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), + createdAt: new Date( + `2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`, + ), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", @@ -482,7 +536,9 @@ describe("agent live run routes", () => { const selectCalls: Array> = []; const db = { select: vi.fn().mockImplementation(() => { - const limitFn = vi.fn(async (value: number) => liveRows.slice(0, value)); + const limitFn = vi.fn(async (value: number) => + liveRows.slice(0, value), + ); const orderedQuery = { limit: limitFn, then: (resolve: (value: typeof liveRows) => unknown) => @@ -499,9 +555,8 @@ describe("agent live run routes", () => { }), }; - const res = await requestApp( - await createApp(db), - (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs"), + const res = await requestApp(await createApp(db), (baseUrl) => + request(baseUrl).get("/api/companies/company-1/live-runs"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); @@ -518,7 +573,9 @@ describe("agent live run routes", () => { triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, - createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), + createdAt: new Date( + `2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`, + ), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", @@ -543,7 +600,9 @@ describe("agent live run routes", () => { triggerDetail: "manual", startedAt: new Date("2026-04-09T09:30:00.000Z"), finishedAt: new Date("2026-04-09T09:35:00.000Z"), - createdAt: new Date(`2026-04-09T09:${String(index % 60).padStart(2, "0")}:00.000Z`), + createdAt: new Date( + `2026-04-09T09:${String(index % 60).padStart(2, "0")}:00.000Z`, + ), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", @@ -581,9 +640,8 @@ describe("agent live run routes", () => { }), }; - const res = await requestApp( - await createApp(db), - (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs?minCount=4"), + const res = await requestApp(await createApp(db), (baseUrl) => + request(baseUrl).get("/api/companies/company-1/live-runs?minCount=4"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); @@ -592,10 +650,11 @@ describe("agent live run routes", () => { }); it("passes scoped wake fields through the legacy heartbeat invoke route", async () => { - const res = await requestApp( - await createApp(), - (baseUrl) => request(baseUrl) - .post(`/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`) + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl) + .post( + `/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`, + ) .send({ reason: "issue_assigned", payload: { @@ -633,10 +692,11 @@ describe("agent live run routes", () => { }); it("calls heartbeat.wakeup with the legacy minimal shape when the body is empty", async () => { - const res = await requestApp( - await createApp(), - (baseUrl) => request(baseUrl) - .post(`/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`) + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl) + .post( + `/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`, + ) .send({}), ); @@ -652,4 +712,171 @@ describe("agent live run routes", () => { }, }); }); + + it("allows implicit local administrators to opt one manual run into raw provider tracing", async () => { + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl) + .post( + `/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`, + ) + .send({ debug: { providerTrace: "raw" } }), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(202); + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith( + routeAgentId, + expect.objectContaining({ + contextSnapshot: expect.objectContaining({ + debug: { providerTrace: "raw" }, + providerTraceRequestedBy: "local-board", + }), + }), + ); + }); + + it("marks traced re-runs as explicit resumes so terminal issue context can execute", async () => { + const issueId = "22222222-2222-4222-8222-222222222222"; + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl) + .post(`/api/agents/${routeAgentId}/wakeup?companyId=company-1`) + .send({ + source: "on_demand", + triggerDetail: "manual", + reason: "rerun_with_provider_trace", + payload: { issueId, taskId: issueId, taskKey: issueId }, + debug: { providerTrace: "raw" }, + }), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(202); + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith( + routeAgentId, + expect.objectContaining({ + contextSnapshot: expect.objectContaining({ + resumeIntent: true, + debug: { providerTrace: "raw" }, + }), + }), + ); + }); + + it("rejects raw provider tracing for ordinary board members", async () => { + const res = await requestApp( + await createApp( + {}, + { + type: "board", + userId: "member-user", + companyIds: ["company-1"], + source: "session", + isInstanceAdmin: false, + }, + ), + (baseUrl) => + request(baseUrl) + .post( + `/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`, + ) + .send({ debug: { providerTrace: "raw" } }), + ); + + expect(res.status).toBe(403); + expect(mockHeartbeatService.wakeup).not.toHaveBeenCalled(); + }); + + it.each([ + ["get", "/api/companies/company-1/provider-traces?runIds=run-1"], + ["get", "/api/heartbeat-runs/run-1/provider-trace"], + ["post", "/api/heartbeat-runs/run-1/provider-trace/frames/1/reveal"], + ["get", "/api/heartbeat-runs/run-1/provider-trace/download"], + ["delete", "/api/heartbeat-runs/run-1/provider-trace"], + ] as const)( + "requires instance administration to %s %s", + async (method, path) => { + const app = await createApp( + {}, + { + type: "board", + userId: "member-user", + companyIds: ["company-1"], + source: "session", + isInstanceAdmin: false, + }, + ); + const res = await requestApp(app, (baseUrl) => + request(baseUrl)[method](path), + ); + + expect(res.status).toBe(403); + expect(mockHeartbeatService.getRun).not.toHaveBeenCalled(); + }, + ); + + it("lists trace status metadata without exposing payload contents", async () => { + mockProviderTraceStore.listMetadataForRuns.mockResolvedValueOnce([ + { + schema: "paperclip.provider_trace_metadata.v1", + id: "trace-1", + runId: "run-1", + companyId: "company-1", + status: "complete", + provider: "codex", + frameCount: 70, + byteCount: 4096, + digest: `sha256:${"a".repeat(64)}`, + reason: null, + requestedBy: "local-board", + createdAt: new Date("2026-08-22T12:00:00.000Z"), + expiresAt: new Date("2026-08-23T12:00:00.000Z"), + deletedAt: null, + }, + ]); + + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl).get( + "/api/companies/company-1/provider-traces?runIds=run-1", + ), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(mockProviderTraceStore.listMetadataForRuns).toHaveBeenCalledWith( + "company-1", + ["run-1"], + ); + expect(res.body[0]).not.toHaveProperty("rawBase64"); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "provider_trace.metadata_listed", + details: expect.objectContaining({ payloadLogged: false }), + }), + ); + }); + + it("returns only the redacted inspection view and audits the access", async () => { + mockProviderTraceStore.inspect.mockResolvedValue({ + trace: { id: "trace-1", status: "complete" }, + entries: [ + { + kind: "frame", + frameId: 1, + parsed: { token: "[withheld]" }, + withheldPaths: ["token"], + }, + ], + }); + const res = await requestApp(await createApp(), (baseUrl) => + request(baseUrl).get("/api/heartbeat-runs/run-1/provider-trace"), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(res.body.entries[0]).not.toHaveProperty("rawBase64"); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "provider_trace.redacted_viewed", + details: { traceId: "trace-1", rawPayloadRevealed: false }, + }), + ); + }); }); diff --git a/server/src/__tests__/agent-permissions-routes.test.ts b/server/src/__tests__/agent-permissions-routes.test.ts index c864762f8f5..dfe301819bd 100644 --- a/server/src/__tests__/agent-permissions-routes.test.ts +++ b/server/src/__tests__/agent-permissions-routes.test.ts @@ -522,6 +522,46 @@ describe.sequential("agent permission routes", () => { expect(res.status).toBe(403); }); + it("requires instance administration to enable agent-scoped raw provider traces", async () => { + const app = await createApp({ + type: "board", + userId: "agent-admin-user", + source: "session", + isInstanceAdmin: false, + companyIds: [companyId], + }); + + const res = await requestApp(app, (baseUrl) => request(baseUrl) + .patch(`/api/agents/${agentId}`) + .send({ runtimeConfig: { debug: { providerTrace: "raw" } } })); + + expect(res.status).toBe(403); + expect(mockAgentService.update).not.toHaveBeenCalled(); + }); + + it("allows instance administrators to enable agent-scoped raw provider traces", async () => { + const app = await createApp({ + type: "board", + userId: "instance-admin-user", + source: "session", + isInstanceAdmin: true, + companyIds: [companyId], + }); + + const res = await requestApp(app, (baseUrl) => request(baseUrl) + .patch(`/api/agents/${agentId}`) + .send({ runtimeConfig: { debug: { providerTrace: "raw" } } })); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(mockAgentService.update).toHaveBeenCalledWith( + agentId, + expect.objectContaining({ + runtimeConfig: { debug: { providerTrace: "raw" } }, + }), + expect.anything(), + ); + }); + it("blocks api key creation for authenticated company members without agent admin permission", async () => { mockAccessService.canUser.mockResolvedValue(false); diff --git a/server/src/__tests__/heartbeat-run-summary.test.ts b/server/src/__tests__/heartbeat-run-summary.test.ts index 84415052029..290e22c655d 100644 --- a/server/src/__tests__/heartbeat-run-summary.test.ts +++ b/server/src/__tests__/heartbeat-run-summary.test.ts @@ -2,8 +2,11 @@ import { describe, expect, it } from "vitest"; import { summarizeHeartbeatRunResultJson, buildHeartbeatRunIssueComment, + LEGACY_WITHHELD_RUN_COMMENT, + projectHistoricalHeartbeatRunComment, findHeartbeatRunCompletionComment, mergeHeartbeatRunResultJson, + resolveHeartbeatRunResponse, } from "../services/heartbeat-run-summary.js"; describe("summarizeHeartbeatRunResultJson", () => { @@ -40,8 +43,15 @@ describe("summarizeHeartbeatRunResultJson", () => { it("returns null for non-object and irrelevant payloads", () => { expect(summarizeHeartbeatRunResultJson(null)).toBeNull(); - expect(summarizeHeartbeatRunResultJson(["nope"] as unknown as Record)).toBeNull(); - expect(summarizeHeartbeatRunResultJson({ nested: { only: "ignored" } })).toBeNull(); + expect( + summarizeHeartbeatRunResultJson(["nope"] as unknown as Record< + string, + unknown + >), + ).toBeNull(); + expect( + summarizeHeartbeatRunResultJson({ nested: { only: "ignored" } }), + ).toBeNull(); }); }); @@ -58,7 +68,9 @@ describe("buildHeartbeatRunIssueComment", () => { it("falls back to result or message when summary is missing", () => { expect(buildHeartbeatRunIssueComment({ result: "done" })).toBe("done"); - expect(buildHeartbeatRunIssueComment({ message: "completed" })).toBe("completed"); + expect(buildHeartbeatRunIssueComment({ message: "completed" })).toBe( + "completed", + ); }); it("returns null when there is no usable final text", () => { @@ -70,8 +82,7 @@ describe("buildHeartbeatRunIssueComment", () => { "Let me check the issue thread first. I'll fetch the latest comments and then decide what to do next."; const comment = buildHeartbeatRunIssueComment({ summary: narration }); - expect(comment).not.toContain("Let me check"); - expect(comment).toContain("did not post a summary comment"); + expect(comment).toBeNull(); }); it("suppresses each narration opener variant", () => { @@ -88,9 +99,7 @@ describe("buildHeartbeatRunIssueComment", () => { "Now I'll push the follow-up commit.", "Next, I'll re-run the suite.", ]) { - expect(buildHeartbeatRunIssueComment({ summary: opener })).toContain( - "did not post a summary comment", - ); + expect(buildHeartbeatRunIssueComment({ summary: opener })).toBeNull(); } }); @@ -101,21 +110,150 @@ describe("buildHeartbeatRunIssueComment", () => { } }); - it("suppresses over-long fallback summaries even without a narration opener", () => { - const comment = buildHeartbeatRunIssueComment({ summary: "x".repeat(1201) }); - expect(comment).toContain("did not post a summary comment"); - expect(comment).not.toContain("xxxx"); + it("never suppresses a response because of its length", () => { + const summary = "x".repeat(20_000); + expect(buildHeartbeatRunIssueComment({ summary })).toBe(summary); }); it("posts a clean, in-length summary with no narration opener normally", () => { - const summary = "## Summary\n\n- fixed the fallback gate\n- added regression tests"; + const summary = + "## Summary\n\n- fixed the fallback gate\n- added regression tests"; expect(buildHeartbeatRunIssueComment({ summary })).toBe(summary); }); - it("posts a summary exactly at the length cap", () => { - const summary = "S" + "x".repeat(1199); - expect(summary.length).toBe(1200); - expect(buildHeartbeatRunIssueComment({ summary })).toBe(summary); + it("uses an accepted semantic result even when it resembles narration", () => { + const summary = + "Let me give you the complete recipe now.\n\n" + "x".repeat(1_420); + expect( + buildHeartbeatRunIssueComment({ + summary: "", + nativeResult: { + schema: "paperclip.run_result.v1", + reportedWorkDisposition: "done", + summary, + }, + }), + ).toBe(summary); + }); +}); + +describe("resolveHeartbeatRunResponse", () => { + const resultJson = { + nativeResult: { + schema: "paperclip.run_result.v1", + reportedWorkDisposition: "done", + summary: "semantic result", + }, + }; + + it("applies comment, final-message, and semantic-result precedence", () => { + expect( + resolveHeartbeatRunResponse({ + resultJson, + existingComment: { id: "comment-1", body: "posted response" }, + finalAgentMessage: { + text: "provider response", + sourceEventId: "event-1", + }, + }), + ).toMatchObject({ + text: "posted response", + decision: { + chosenSource: "existing_issue_comment", + commentAction: "reuse", + commentId: "comment-1", + }, + }); + + expect( + resolveHeartbeatRunResponse({ + resultJson, + finalAgentMessage: { + text: "provider response", + sourceEventId: "event-1", + }, + }), + ).toMatchObject({ + text: "provider response", + decision: { + chosenSource: "final_agent_message", + sourceEventId: "event-1", + }, + }); + + expect(resolveHeartbeatRunResponse({ resultJson })).toMatchObject({ + text: "semantic result", + decision: { chosenSource: "semantic_result_summary" }, + }); + }); + + it("returns no response instead of an artificial placeholder", () => { + expect(resolveHeartbeatRunResponse({ resultJson: null })).toMatchObject({ + text: null, + decision: { chosenSource: "none", commentAction: "none" }, + }); + }); + + it("does not render a serialized semantic result as the final prose", () => { + expect( + resolveHeartbeatRunResponse({ + resultJson, + finalAgentMessage: { + text: JSON.stringify(resultJson.nativeResult), + sourceEventId: "event-structured-result", + }, + }), + ).toMatchObject({ + text: "semantic result", + decision: { chosenSource: "semantic_result_summary" }, + }); + }); + + it("does not let an empty issue comment hide an upstream response", () => { + expect( + resolveHeartbeatRunResponse({ + resultJson, + existingComment: { id: "empty-comment", body: " " }, + }), + ).toMatchObject({ + text: "semantic result", + decision: { chosenSource: "semantic_result_summary" }, + }); + }); + + it("preserves the exact upstream response text", () => { + const text = "\n final response with intentional whitespace \n"; + expect( + resolveHeartbeatRunResponse({ + resultJson, + finalAgentMessage: { text, sourceEventId: "event-exact" }, + }).text, + ).toBe(text); + }); +}); + +describe("projectHistoricalHeartbeatRunComment", () => { + it("projects the accepted semantic response over the known placeholder", () => { + const summary = "# Full recipe\n\n" + "ribs ".repeat(400); + expect( + projectHistoricalHeartbeatRunComment(LEGACY_WITHHELD_RUN_COMMENT, { + nativeResult: { + schema: "paperclip.run_result.v1", + summary, + }, + }), + ).toBe(summary); + }); + + it("does not rewrite ordinary historical comments", () => { + expect( + projectHistoricalHeartbeatRunComment("Real response", { + nativeResult: { + schema: "paperclip.run_result.v1", + summary: "Different response", + }, + }), + ).toBe("Real response"); }); }); @@ -132,13 +270,19 @@ describe("findHeartbeatRunCompletionComment", () => { }, }; - expect(findHeartbeatRunCompletionComment([progress], resultJson)).toBeNull(); - expect(findHeartbeatRunCompletionComment([final, progress], resultJson)).toEqual(final); + expect( + findHeartbeatRunCompletionComment([progress], resultJson), + ).toBeNull(); + expect( + findHeartbeatRunCompletionComment([final, progress], resultJson), + ).toEqual(final); }); it("preserves the fallback-only behavior for ordinary agent comments", () => { const comment = { id: "manual-comment" }; - expect(findHeartbeatRunCompletionComment([comment], { summary: "done" })).toEqual(comment); + expect( + findHeartbeatRunCompletionComment([comment], { summary: "done" }), + ).toEqual(comment); }); }); @@ -154,11 +298,15 @@ describe("mergeHeartbeatRunResultJson", () => { stderr: "", summary: "## Summary\n\n1. first thing\n2. second thing", }); - expect(buildHeartbeatRunIssueComment(merged)).toBe("## Summary\n\n1. first thing\n2. second thing"); + expect(buildHeartbeatRunIssueComment(merged)).toBe( + "## Summary\n\n1. first thing\n2. second thing", + ); }); it("creates a result payload when only a summary exists", () => { - expect(mergeHeartbeatRunResultJson(null, "done")).toEqual({ summary: "done" }); + expect(mergeHeartbeatRunResultJson(null, "done")).toEqual({ + summary: "done", + }); }); it("does not overwrite an explicit summary already returned by the adapter", () => { diff --git a/server/src/__tests__/provider-trace-store.test.ts b/server/src/__tests__/provider-trace-store.test.ts new file mode 100644 index 00000000000..f07ecccd66c --- /dev/null +++ b/server/src/__tests__/provider-trace-store.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from "vitest"; +import { createHash } from "node:crypto"; +import { + assessProviderTraceEntries, + redactProviderTraceFrame, +} from "../services/provider-trace-store.js"; + +function frame(payload: unknown) { + return { + kind: "frame", + schema: "paperclip.provider_trace_frame.v1", + frameId: 1, + rawBase64: Buffer.from(JSON.stringify(payload)).toString("base64"), + }; +} + +describe("provider trace redaction", () => { + it("removes exact bytes and masks secret-shaped fields and values", () => { + const redacted = redactProviderTraceFrame( + frame({ + authorization: "Bearer exact-token-value", + message: "sk-abcdefghijklmnopqrstuvwxyz", + nested: { apiKey: "also-secret", safe: "visible" }, + }), + ); + + expect(redacted).not.toHaveProperty("rawBase64"); + expect(redacted.parsed).toEqual({ + authorization: "[withheld]", + message: "[withheld]", + nested: { apiKey: "[withheld]", safe: "visible" }, + }); + expect(redacted.withheldPaths).toEqual([ + "authorization", + "message", + "nested.apiKey", + ]); + }); + + it("withholds reasoning item content while retaining routing metadata", () => { + const redacted = redactProviderTraceFrame( + frame({ + item: { + id: "reason-1", + type: "reasoning", + status: "completed", + summary: ["private chain"], + encrypted_content: "ciphertext", + }, + }), + ); + + expect(redacted.parsed).toEqual({ + item: { + id: "reason-1", + type: "reasoning", + status: "completed", + summary: "[withheld]", + encrypted_content: "[withheld]", + }, + }); + expect(redacted.withheldPaths).toContain("item.summary"); + }); +}); + +describe("provider trace channel integrity", () => { + function completeChannel(channel: string, raw = Buffer.from("{}")) { + return [ + { + kind: "frame", + debugChannel: channel, + debugSequence: 1, + frameId: 1, + rawBase64: raw.toString("base64"), + byteLength: raw.byteLength, + digest: `sha256:${createHash("sha256").update(raw).digest("hex")}`, + }, + { + kind: "trace_status", + debugChannel: channel, + debugSequence: 2, + acknowledgedDebugSequence: 1, + status: "complete", + reason: null, + }, + ]; + } + + it("accepts independently sequenced and acknowledged debug channels", () => { + expect( + assessProviderTraceEntries([ + ...completeChannel("rust_native"), + ...completeChannel("typescript_runnerd_rehydration"), + ]), + ).toEqual({ status: "complete", reason: null }); + }); + + it("marks gaps, missing acknowledgements, and digest changes incomplete", () => { + const gap = completeChannel("rust_native"); + gap[1]!.debugSequence = 3; + expect(assessProviderTraceEntries(gap).reason).toBe( + "trace_debug_sequence_gap:rust_native", + ); + + const noAck = completeChannel("rust_native").slice(0, 1); + expect(assessProviderTraceEntries(noAck).reason).toBe( + "trace_channel_ack_missing:rust_native", + ); + + const changed = completeChannel("rust_native"); + changed[0]!.rawBase64 = Buffer.from("changed").toString("base64"); + expect(assessProviderTraceEntries(changed).reason).toBe( + "provider_frame_digest_mismatch", + ); + }); +}); diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 2079641d6c0..fe2c8a67150 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -30,6 +30,7 @@ import { testEnvironment as claudeTestEnvironment, sessionCodec as claudeSessionCodec, getQuotaWindows as claudeGetQuotaWindows, + readClaudeAuthStatus, getConfigSchema as getClaudeConfigSchema, CLAUDE_SETUP_TOKEN_COMMAND, parseSetupTokenPrompt, @@ -47,6 +48,7 @@ import { testEnvironment as codexTestEnvironment, sessionCodec as codexSessionCodec, getQuotaWindows as codexGetQuotaWindows, + readCodexAuthInfo, getConfigSchema as getCodexConfigSchema, CODEX_DEVICE_LOGIN_COMMAND, parseDeviceLoginPrompt, @@ -244,11 +246,17 @@ async function testPaperclipRunnerAcpxEnvironment( : {}; const environment = { ...process.env }; for (const [key, value] of Object.entries(configuredEnv)) if (typeof value === "string") environment[key] = value; + const managedClaudeAuth = agent === "claude" && !environment.ANTHROPIC_API_KEY && !environment.CLAUDE_CODE_OAUTH_TOKEN + ? await readClaudeAuthStatus() + : null; + const managedCodexAuth = agent === "codex" && !environment.OPENAI_API_KEY && !environment.CODEX_API_KEY + ? await readCodexAuthInfo() + : null; const credentialReady = agent === "pi" ? Boolean(environment.OPENROUTER_API_KEY) : agent === "claude" - ? Boolean(environment.ANTHROPIC_API_KEY || environment.CLAUDE_CODE_OAUTH_TOKEN) - : Boolean(environment.OPENAI_API_KEY || environment.CODEX_API_KEY); + ? Boolean(environment.ANTHROPIC_API_KEY || environment.CLAUDE_CODE_OAUTH_TOKEN || managedClaudeAuth?.loggedIn) + : Boolean(environment.OPENAI_API_KEY || environment.CODEX_API_KEY || managedCodexAuth); if (!credentialReady) { const required = agent === "pi" ? "OPENROUTER_API_KEY" diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index a52841c960d..60983edd05d 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -1,8 +1,19 @@ -import { Router, type NextFunction, type Request, type Response } from "express"; +import { + Router, + type NextFunction, + type Request, + type Response, +} from "express"; import { generateKeyPairSync, randomUUID } from "node:crypto"; import path from "node:path"; import type { Db } from "@paperclipai/db"; -import { agents as agentsTable, companies, heartbeatRuns, issues as issuesTable, projects as projectsTable } from "@paperclipai/db"; +import { + agents as agentsTable, + companies, + heartbeatRuns, + issues as issuesTable, + projects as projectsTable, +} from "@paperclipai/db"; import { and, desc, eq, inArray, not, sql } from "drizzle-orm"; import { agentSkillSyncSchema, @@ -59,9 +70,25 @@ import { syncInstructionsBundleConfigFromFilePath, workspaceOperationService, } from "../services/index.js"; -import { badRequest, conflict, forbidden, HttpError, notFound, unprocessable } from "../errors.js"; +import { + badRequest, + conflict, + forbidden, + HttpError, + notFound, + unprocessable, +} from "../errors.js"; import { createRunSecretRedactionRegistry } from "../services/run-secret-redaction.js"; -import { assertAuthenticated, assertBoard, assertCompanyAccess, assertInstanceAdmin, buildActorSecretContext, getAccessibleResource, getActorInfo, hasCompanyAccess } from "./authz.js"; +import { + assertAuthenticated, + assertBoard, + assertCompanyAccess, + assertInstanceAdmin, + buildActorSecretContext, + getAccessibleResource, + getActorInfo, + hasCompanyAccess, +} from "./authz.js"; import { runAdapterLoginStartSpine } from "./adapter-login-route-spine.js"; import { assertNoAgentHostWorkspaceCommandMutation, @@ -82,6 +109,7 @@ import { getDisabledAdapterTypes } from "../services/adapter-plugin-store.js"; import { skillVersionSelectionMap } from "../services/runtime-skill-selections.js"; import { secretService } from "../services/secrets.js"; import { authorizationDeniedDetails } from "../services/authorization.js"; +import { providerTraceStore } from "../services/provider-trace-store.js"; import { detectAdapterModel, findActiveServerAdapter, @@ -104,7 +132,13 @@ import { queueRunnerPrpRuntimeRequestResolution, RunnerPrpRuntimeRequestResolutionError, } from "../realtime/runner-prp-ws.js"; -import { renderOrgChartSvg, renderOrgChartPng, type OrgNode, type OrgChartStyle, ORG_CHART_STYLES } from "./org-chart-svg.js"; +import { + renderOrgChartSvg, + renderOrgChartPng, + type OrgNode, + type OrgChartStyle, + ORG_CHART_STYLES, +} from "./org-chart-svg.js"; import { instanceSettingsService, isTruthyRuntimeEnvValue, @@ -182,11 +216,15 @@ import { const AGENT_SKILL_ASSIGNMENT_MODES = ["add", "remove", "replace"] as const; -function requireAgentSkillAssignmentMode(req: Request, _res: Response, next: NextFunction) { +function requireAgentSkillAssignmentMode( + req: Request, + _res: Response, + next: NextFunction, +) { if (!AGENT_SKILL_ASSIGNMENT_MODES.includes(req.body?.mode)) { throw unprocessable( - 'Skill sync requires mode: "add", "remove", or "replace". ' - + 'Use "replace" only to overwrite the complete desired skill set.', + 'Skill sync requires mode: "add", "remove", or "replace". ' + + 'Use "replace" only to overwrite the complete desired skill set.', ); } next(); @@ -227,10 +265,13 @@ function readLiveRunsQueryInt(value: unknown, max: number, fallback = 0) { function readRunIssueId(context: Record | null) { const directIssueId = context?.issueId; - if (typeof directIssueId === "string" && isUuidLike(directIssueId)) return directIssueId; + if (typeof directIssueId === "string" && isUuidLike(directIssueId)) + return directIssueId; const paperclipIssue = readObject(context?.paperclipIssue); const nestedIssueId = paperclipIssue?.id; - return typeof nestedIssueId === "string" && isUuidLike(nestedIssueId) ? nestedIssueId : null; + return typeof nestedIssueId === "string" && isUuidLike(nestedIssueId) + ? nestedIssueId + : null; } export function agentRoutes( @@ -290,12 +331,15 @@ export function agentRoutes( cursor: "instructionsFilePath", pi_local: "instructionsFilePath", }; - const DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES = new Set(Object.keys(DEFAULT_INSTRUCTIONS_PATH_KEYS)); + const DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES = new Set( + Object.keys(DEFAULT_INSTRUCTIONS_PATH_KEYS), + ); /** Check if an adapter supports the managed instructions bundle. */ function adapterSupportsInstructionsBundle(adapterType: string): boolean { const adapter = findActiveServerAdapter(adapterType); - if (adapter?.supportsInstructionsBundle !== undefined) return adapter.supportsInstructionsBundle; + if (adapter?.supportsInstructionsBundle !== undefined) + return adapter.supportsInstructionsBundle; return DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES.has(adapterType); } @@ -303,11 +347,15 @@ export function agentRoutes( function resolveInstructionsPathKey(adapterType: string): string | null { const adapter = findActiveServerAdapter(adapterType); if (adapter?.instructionsPathKey) return adapter.instructionsPathKey; - if (adapter?.supportsInstructionsBundle === true) return "instructionsFilePath"; + if (adapter?.supportsInstructionsBundle === true) + return "instructionsFilePath"; if (adapter?.supportsInstructionsBundle === false) return null; return DEFAULT_INSTRUCTIONS_PATH_KEYS[adapterType] ?? null; } - const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set(["instructionsFilePath", "agentsMdPath"]); + const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set([ + "instructionsFilePath", + "agentsMdPath", + ]); const KNOWN_INSTRUCTIONS_BUNDLE_KEYS = [ "instructionsBundleMode", "instructionsRootPath", @@ -315,7 +363,9 @@ export function agentRoutes( "instructionsFilePath", "agentsMdPath", ] as const; - const KNOWN_INSTRUCTIONS_BUNDLE_KEY_SET: ReadonlySet = new Set(KNOWN_INSTRUCTIONS_BUNDLE_KEYS); + const KNOWN_INSTRUCTIONS_BUNDLE_KEY_SET: ReadonlySet = new Set( + KNOWN_INSTRUCTIONS_BUNDLE_KEYS, + ); const router = Router(); const svc = agentService(db); @@ -347,7 +397,10 @@ export function agentRoutes( }; // Rate-limit the start route: a small window per company and owner (SR-4). - const setupTokenRateLimiter = createInviteRateLimiter({ windowMs: 60_000, maxRequests: 5 }); + const setupTokenRateLimiter = createInviteRateLimiter({ + windowMs: 60_000, + maxRequests: 5, + }); // The deferred lease manager. It fails closed on acquire until a caller binds // the live transport. It still releases a lease by handle or by id, so a @@ -369,11 +422,14 @@ export function agentRoutes( // The in-memory non-secret cleanup record store. It is the default store when a // caller does not inject a durable database-backed store. const setupTokenCleanupRows = new Map(); - const scopeMatchesRow = (row: SetupTokenCleanupRecord, identity: { - companyId: string; - ownerUserId: string; - adapterType: string; - }): boolean => + const scopeMatchesRow = ( + row: SetupTokenCleanupRecord, + identity: { + companyId: string; + ownerUserId: string; + adapterType: string; + }, + ): boolean => row.companyId === identity.companyId && row.ownerUserId === identity.ownerUserId && row.adapterType === identity.adapterType; @@ -389,12 +445,15 @@ export function agentRoutes( // The delete matches the full owner scope, so it never removes a row by the // session id alone. const row = setupTokenCleanupRows.get(identity.sessionId); - if (row && scopeMatchesRow(row, identity)) setupTokenCleanupRows.delete(identity.sessionId); + if (row && scopeMatchesRow(row, identity)) + setupTokenCleanupRows.delete(identity.sessionId); }, async listReapable(): Promise { return []; }, - async consumeStoredClaim(identity): Promise { + async consumeStoredClaim( + identity, + ): Promise { const row = setupTokenCleanupRows.get(identity.sessionId); if ( !row || @@ -433,7 +492,8 @@ export function agentRoutes( const setupTokenCleanupStore = options.setupTokenLogin?.store ?? inMemorySetupTokenCleanupStore; const setupTokenSecretWriter = - options.setupTokenLogin?.completeCredential ?? deferredSetupTokenSecretWriter; + options.setupTokenLogin?.completeCredential ?? + deferredSetupTokenSecretWriter; // Re-check the environment company binding at lease acquisition. The start // route runs `assertSandboxLoginEnvironment` before the session begins, but @@ -445,9 +505,13 @@ export function agentRoutes( // insert, so a bind that lands during the provider call still holds no lease. const guardedSetupTokenLeaseManager: SetupTokenLeaseManager = { async acquire(input): Promise { - await assertSandboxLoginEnvironment(input.scope.companyId, input.scope.environmentId, { - requireSetupTokenLoginProvider: true, - }); + await assertSandboxLoginEnvironment( + input.scope.companyId, + input.scope.environmentId, + { + requireSetupTokenLoginProvider: true, + }, + ); return setupTokenLeaseManager.acquire(input); }, release: (lease) => setupTokenLeaseManager.release(lease), @@ -465,7 +529,9 @@ export function agentRoutes( { // Log the startup transport assessment, so an operator can see whether a // forwarded proxy protocol is trusted for the confidential routes (SR-7). - const startupAssessment = assessConfidentialStartup(setupTokenConfidentialConfig); + const startupAssessment = assessConfidentialStartup( + setupTokenConfidentialConfig, + ); logger.info( { proxyForwardingEnabled: startupAssessment.proxyForwardingEnabled, @@ -482,6 +548,11 @@ export function agentRoutes( const heartbeat = heartbeatService(db, { pluginWorkerManager: options.pluginWorkerManager, }); + const providerTraces = providerTraceStore(db); + const traceExpiryCleanup = providerTraces.cleanupExpired?.(); + void traceExpiryCleanup?.catch((error) => { + logger.warn({ error }, "provider trace expiry cleanup failed"); + }); const recovery = recoveryService(db, { enqueueWakeup: heartbeat.wakeup }); const issueApprovalsSvc = issueApprovalService(db); const secretsSvc = secretService(db); @@ -489,7 +560,8 @@ export function agentRoutes( const companySkills = companySkillService(db); const workspaceOperations = workspaceOperationService(db); const instanceSettings = instanceSettingsService(db); - const strictSecretsMode = process.env.PAPERCLIP_SECRETS_STRICT_MODE === "true"; + const strictSecretsMode = + process.env.PAPERCLIP_SECRETS_STRICT_MODE === "true"; // The company-scoped adapter login-session service. It runs the device-login // flow in a fresh trusted sandbox and holds the one-time prompt in memory. The @@ -531,7 +603,10 @@ export function agentRoutes( // inside the lock, so it observes a reaper reclaim that committed // before this section acquired the lock. const row = await adapterLoginStore.get(context.sessionId); - return row?.status === "promoting" && row.companyId === context.companyId; + return ( + row?.status === "promoting" && + row.companyId === context.companyId + ); }, log: (line) => { // The promotion lines carry no token bytes and no raw account id, @@ -556,7 +631,9 @@ export function agentRoutes( ); } if (outcome !== "promoted" && outcome !== "kept") { - throw new Error(`device-login credential promotion rejected: ${outcome}`); + throw new Error( + `device-login credential promotion rejected: ${outcome}`, + ); } }, }, @@ -575,23 +652,41 @@ export function agentRoutes( environmentId: string | null | undefined, ) { if (environmentId === undefined || environmentId === null) return; - await assertEnvironmentSelectionForCompany(environmentService(db), companyId, environmentId, { - allowedDrivers: allowedEnvironmentDriversForAgent(adapterType), - }); + await assertEnvironmentSelectionForCompany( + environmentService(db), + companyId, + environmentId, + { + allowedDrivers: allowedEnvironmentDriversForAgent(adapterType), + }, + ); } - async function decideAgentRead(req: Request, agent: { id: string; companyId: string }) { + async function decideAgentRead( + req: Request, + agent: { id: string; companyId: string }, + ) { return access.decide({ actor: req.actor, action: "agent:read", - resource: { type: "agent", companyId: agent.companyId, agentId: agent.id }, + resource: { + type: "agent", + companyId: agent.companyId, + agentId: agent.id, + }, }); } - async function assertAgentReadAllowed(req: Request, res: Response, agent: { id: string; companyId: string }) { + async function assertAgentReadAllowed( + req: Request, + res: Response, + agent: { id: string; companyId: string }, + ) { const decision = await decideAgentRead(req, agent); if (decision.allowed) return true; - res.status(403).json({ error: "Agent is outside this actor's authorization boundary" }); + res + .status(403) + .json({ error: "Agent is outside this actor's authorization boundary" }); return false; } @@ -600,12 +695,17 @@ export function agentRoutes( rows: T[], fallbackCompanyId?: string, ) { - const decisions = await Promise.all(rows.map((agent) => { - const id = typeof agent.id === "string" ? agent.id : null; - const companyId = typeof agent.companyId === "string" ? agent.companyId : fallbackCompanyId ?? null; - if (!id || !companyId) return Promise.resolve({ allowed: false }); - return decideAgentRead(req, { id, companyId }); - })); + const decisions = await Promise.all( + rows.map((agent) => { + const id = typeof agent.id === "string" ? agent.id : null; + const companyId = + typeof agent.companyId === "string" + ? agent.companyId + : (fallbackCompanyId ?? null); + if (!id || !companyId) return Promise.resolve({ allowed: false }); + return decideAgentRead(req, { id, companyId }); + }), + ); return rows.filter((_, index) => decisions[index]?.allowed); } @@ -656,7 +756,8 @@ export function agentRoutes( { code: "environment_not_found", level: "warn", - message: "Selected environment was not found. The test did not run.", + message: + "Selected environment was not found. The test did not run.", }, ], release: noopRelease, @@ -700,8 +801,7 @@ export function agentRoutes( { code: "environment_target_unavailable", level: "warn", - message: - `Could not resolve an execution target for environment "${environment.name}". The test did not run.`, + message: `Could not resolve an execution target for environment "${environment.name}". The test did not run.`, }, ], release: noopRelease, @@ -714,8 +814,7 @@ export function agentRoutes( { code: "environment_target_failed", level: "warn", - message: - `Could not connect to environment "${environment.name}" to run the test.`, + message: `Could not connect to environment "${environment.name}" to run the test.`, detail: err instanceof Error ? err.message : String(err), }, ], @@ -738,17 +837,20 @@ export function agentRoutes( // agent lease) and archive it on release instead of deleting it, so the // operator can inspect the exact sandbox from the provider dashboard while // provider-side expiry reaps it later. - const testEnvironment = environment.driver === "sandbox" - ? { - ...environment, - config: { - ...(environment.config ?? {}), - reuseLease: false, - archiveOnRelease: true, - }, - } - : environment; - let leaseRecord: Awaited>; + const testEnvironment = + environment.driver === "sandbox" + ? { + ...environment, + config: { + ...(environment.config ?? {}), + reuseLease: false, + archiveOnRelease: true, + }, + } + : environment; + let leaseRecord: Awaited< + ReturnType + >; try { leaseRecord = await environmentRuntime.acquireRunLease({ companyId: input.companyId, @@ -834,10 +936,10 @@ export function agentRoutes( let target: AdapterExecutionTarget | null; try { // Prefer the cwd the realize step returned; fall back to lease metadata. - const leaseMetadataForTarget: Record | null = - realizedCwd - ? { ...(leaseRecord.lease.metadata ?? {}), remoteCwd: realizedCwd } - : (leaseRecord.lease.metadata as Record | null) ?? null; + const leaseMetadataForTarget: Record | null = realizedCwd + ? { ...(leaseRecord.lease.metadata ?? {}), remoteCwd: realizedCwd } + : ((leaseRecord.lease.metadata as Record | null) ?? + null); target = await resolveEnvironmentExecutionTarget({ db, @@ -879,8 +981,7 @@ export function agentRoutes( { code: "environment_target_unsupported", level: "warn", - message: - `Adapter "${input.adapterType}" is not allowed in "${environment.name}" environments.`, + message: `Adapter "${input.adapterType}" is not allowed in "${environment.name}" environments.`, }, ], release: noopRelease, @@ -899,10 +1000,14 @@ export function agentRoutes( }; } - function readMetadataString(metadata: Record, keys: string[]): string | null { + function readMetadataString( + metadata: Record, + keys: string[], + ): string | null { for (const key of keys) { const value = metadata[key]; - if (typeof value === "string" && value.trim().length > 0) return value.trim(); + if (typeof value === "string" && value.trim().length > 0) + return value.trim(); } return null; } @@ -917,9 +1022,19 @@ export function agentRoutes( }; }): AdapterEnvironmentCheck { const metadata = input.lease.metadata ?? {}; - const provider = input.lease.provider ?? readMetadataString(metadata, ["provider"]); - const sandboxId = readMetadataString(metadata, ["sandboxId", "sandboxID", "sandbox_id", "id"]); - const sandboxName = readMetadataString(metadata, ["sandboxName", "sandbox_name", "name"]); + const provider = + input.lease.provider ?? readMetadataString(metadata, ["provider"]); + const sandboxId = readMetadataString(metadata, [ + "sandboxId", + "sandboxID", + "sandbox_id", + "id", + ]); + const sandboxName = readMetadataString(metadata, [ + "sandboxName", + "sandbox_name", + "name", + ]); const snapshotRef = readMetadataString(metadata, [ "snapshot", "snapshotId", @@ -944,11 +1059,15 @@ export function agentRoutes( ]); const detailParts = [ `paperclipLeaseId=${input.lease.id}`, - input.lease.providerLeaseId ? `providerLeaseId=${input.lease.providerLeaseId}` : null, + input.lease.providerLeaseId + ? `providerLeaseId=${input.lease.providerLeaseId}` + : null, provider ? `provider=${provider}` : null, sandboxId ? `sandboxId=${sandboxId}` : null, sandboxName ? `sandboxName=${sandboxName}` : null, - snapshotRef ? `${templateKind ? `${templateKind}Ref` : "snapshotOrTemplateRef"}=${snapshotRef}` : null, + snapshotRef + ? `${templateKind ? `${templateKind}Ref` : "snapshotOrTemplateRef"}=${snapshotRef}` + : null, ].filter((part): part is string => Boolean(part)); return { @@ -966,17 +1085,31 @@ export function agentRoutes( }; } - function canCreateAgents(agent: { role: string; permissions: Record | null | undefined }) { - if (!agent.permissions || typeof agent.permissions !== "object") return false; - return Boolean((agent.permissions as Record).canCreateAgents); + function canCreateAgents(agent: { + role: string; + permissions: Record | null | undefined; + }) { + if (!agent.permissions || typeof agent.permissions !== "object") + return false; + return Boolean( + (agent.permissions as Record).canCreateAgents, + ); } - async function buildAgentAccessState(agent: NonNullable>>) { - const membership = await access.getMembership(agent.companyId, "agent", agent.id); + async function buildAgentAccessState( + agent: NonNullable>>, + ) { + const membership = await access.getMembership( + agent.companyId, + "agent", + agent.id, + ); const grants = membership ? await access.listPrincipalGrants(agent.companyId, "agent", agent.id) : []; - const hasExplicitTaskAssignGrant = grants.some((grant) => grant.permissionKey === "tasks:assign"); + const hasExplicitTaskAssignGrant = grants.some( + (grant) => grant.permissionKey === "tasks:assign", + ); if (agent.role === "ceo") { return { @@ -1038,22 +1171,32 @@ export function agentRoutes( }; } - async function resolveAgentSelfTrustPreset(req: Request, agent: NonNullable>>) { + async function resolveAgentSelfTrustPreset( + req: Request, + agent: NonNullable>>, + ) { if (req.actor.type !== "agent" || req.actor.agentId !== agent.id) { return { kind: "standard" as const }; } - const run = req.actor.type === "agent" && req.actor.runId - ? await db - .select({ - companyId: heartbeatRuns.companyId, - agentId: heartbeatRuns.agentId, - contextSnapshot: heartbeatRuns.contextSnapshot, - }) - .from(heartbeatRuns) - .where(and(eq(heartbeatRuns.id, req.actor.runId), eq(heartbeatRuns.companyId, agent.companyId))) - .then((rows) => rows[0] ?? null) - : null; - const runContext = run?.agentId === agent.id ? readObject(run.contextSnapshot) : null; + const run = + req.actor.type === "agent" && req.actor.runId + ? await db + .select({ + companyId: heartbeatRuns.companyId, + agentId: heartbeatRuns.agentId, + contextSnapshot: heartbeatRuns.contextSnapshot, + }) + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.id, req.actor.runId), + eq(heartbeatRuns.companyId, agent.companyId), + ), + ) + .then((rows) => rows[0] ?? null) + : null; + const runContext = + run?.agentId === agent.id ? readObject(run.contextSnapshot) : null; const runExecutionPolicy = readObject(runContext?.executionPolicy); const runIssueId = readRunIssueId(runContext); const runScopedIssue = runIssueId @@ -1062,11 +1205,23 @@ export function agentRoutes( companyId: issuesTable.companyId, projectId: issuesTable.projectId, executionPolicy: issuesTable.executionPolicy, - projectExecutionWorkspacePolicy: projectsTable.executionWorkspacePolicy, + projectExecutionWorkspacePolicy: + projectsTable.executionWorkspacePolicy, }) .from(issuesTable) - .leftJoin(projectsTable, and(eq(projectsTable.id, issuesTable.projectId), eq(projectsTable.companyId, issuesTable.companyId))) - .where(and(eq(issuesTable.id, runIssueId), eq(issuesTable.companyId, agent.companyId))) + .leftJoin( + projectsTable, + and( + eq(projectsTable.id, issuesTable.projectId), + eq(projectsTable.companyId, issuesTable.companyId), + ), + ) + .where( + and( + eq(issuesTable.id, runIssueId), + eq(issuesTable.companyId, agent.companyId), + ), + ) .then((rows) => rows[0] ?? null) : null; @@ -1076,7 +1231,8 @@ export function agentRoutes( project: runScopedIssue?.projectId ? { companyId: runScopedIssue.companyId, - executionWorkspacePolicy: runScopedIssue.projectExecutionWorkspacePolicy, + executionWorkspacePolicy: + runScopedIssue.projectExecutionWorkspacePolicy, } : null, issue: runScopedIssue @@ -1085,11 +1241,15 @@ export function agentRoutes( executionPolicy: runScopedIssue.executionPolicy, } : null, - run: runExecutionPolicy ? { companyId: agent.companyId, executionPolicy: runExecutionPolicy } : null, + run: runExecutionPolicy + ? { companyId: agent.companyId, executionPolicy: runExecutionPolicy } + : null, }); } - function buildLowTrustSelfView(agent: NonNullable>>) { + function buildLowTrustSelfView( + agent: NonNullable>>, + ) { return { id: agent.id, companyId: agent.companyId, @@ -1106,7 +1266,13 @@ export function agentRoutes( agentId: string, grantedByUserId: string | null, ) { - await access.ensureMembership(companyId, "agent", agentId, "member", "active"); + await access.ensureMembership( + companyId, + "agent", + agentId, + "member", + "active", + ); await access.setPrincipalPermission( companyId, "agent", @@ -1117,7 +1283,10 @@ export function agentRoutes( ); } - async function assertCanCreateAgentsForCompany(req: Request, companyId: string) { + async function assertCanCreateAgentsForCompany( + req: Request, + companyId: string, + ) { assertCompanyAccess(req, companyId); const decision = await access.decide({ actor: req.actor, @@ -1125,17 +1294,25 @@ export function agentRoutes( resource: { type: "company", companyId }, }); if (!decision.allowed) { - throw forbidden(decision.explanation, authorizationDeniedDetails(decision)); + throw forbidden( + decision.explanation, + authorizationDeniedDetails(decision), + ); } if (req.actor.type !== "agent") return null; - const actorAgent = req.actor.agentId ? await svc.getById(req.actor.agentId) : null; + const actorAgent = req.actor.agentId + ? await svc.getById(req.actor.agentId) + : null; if (!actorAgent || actorAgent.companyId !== companyId) { throw forbidden("Agent key cannot access another company"); } return actorAgent; } - async function assertBoardCanManageAgentsForCompany(req: Request, companyId: string) { + async function assertBoardCanManageAgentsForCompany( + req: Request, + companyId: string, + ) { assertBoard(req); assertCompanyAccess(req, companyId); const decision = await access.decide({ @@ -1166,7 +1343,10 @@ export function agentRoutes( resource: { type: "company", companyId }, }); if (!decision.allowed) { - throw forbidden(decision.explanation, authorizationDeniedDetails(decision)); + throw forbidden( + decision.explanation, + authorizationDeniedDetails(decision), + ); } const userId = req.actor.userId; if (!userId) { @@ -1190,7 +1370,9 @@ export function agentRoutes( // the same transport passes with no code change. It rejects an adapter with no // matching capability with a fixed 400. function assertStreamedExecLoginAdapter(type: string): void { - if (getRegistryLoginCapability(type)?.sandboxTransport !== "streamed_exec") { + if ( + getRegistryLoginCapability(type)?.sandboxTransport !== "streamed_exec" + ) { throw badRequest(`Adapter "${type}" does not support a device login.`); } } @@ -1212,16 +1394,22 @@ export function agentRoutes( environmentId: string, options?: { requireSetupTokenLoginProvider?: boolean }, ): Promise { - await assertEnvironmentSelectionForCompany(environmentsSvc, companyId, environmentId, { - allowedDrivers: ["sandbox"], - }); + await assertEnvironmentSelectionForCompany( + environmentsSvc, + companyId, + environmentId, + { + allowedDrivers: ["sandbox"], + }, + ); // Reject an environment that another company owns. A managed sandbox // environment binds to the companies that the instance provisions it for. // When the environment binds to companies but not the request company, the // environment belongs to another company. A login there runs the process in // a foreign company sandbox, so the guard fails closed. An environment with // no company binding is instance-global and stays open to every member. - const boundCompanyIds = await environmentsSvc.listBoundCompanyIds(environmentId); + const boundCompanyIds = + await environmentsSvc.listBoundCompanyIds(environmentId); if (boundCompanyIds.length > 0 && !boundCompanyIds.includes(companyId)) { throw forbidden("The selected environment belongs to another company.", { code: "environment_company_mismatch", @@ -1247,7 +1435,9 @@ export function agentRoutes( * non-plugin provider, and a provider without the flag all fail closed with * the fixed, typed error, so no session row, lease, or pseudo-terminal starts. */ - async function assertSetupTokenLoginProviderCapability(environmentId: string): Promise { + async function assertSetupTokenLoginProviderCapability( + environmentId: string, + ): Promise { const environment = await environmentsSvc.getById(environmentId); const config = environment?.config && typeof environment.config === "object" @@ -1255,7 +1445,10 @@ export function agentRoutes( : {}; const provider = typeof config.provider === "string" ? config.provider : ""; const resolved = provider - ? await resolvePluginSandboxProviderDriverByKey({ db, driverKey: provider }) + ? await resolvePluginSandboxProviderDriverByKey({ + db, + driverKey: provider, + }) : null; if (!resolved?.driver.supportsLoginPty) { throw unprocessable(SETUP_TOKEN_PROVIDER_UNSUPPORTED, { @@ -1278,11 +1471,22 @@ export function agentRoutes( // Read by the public session id, scoped to the company. The store predicate // already carries the company id, so a foreign-company caller reads nothing // and the internal row id never matches. Keep the adapter and owner checks. - const row = await adapterLoginStore.getByPublicId(publicSessionId, companyId); - if (!row || row.adapterType !== adapterType || row.startedByUserId !== requestingUserId) { + const row = await adapterLoginStore.getByPublicId( + publicSessionId, + companyId, + ); + if ( + !row || + row.adapterType !== adapterType || + row.startedByUserId !== requestingUserId + ) { return null; } - return adapterLoginService.readOwnerSession(publicSessionId, companyId, requestingUserId); + return adapterLoginService.readOwnerSession( + publicSessionId, + companyId, + requestingUserId, + ); } async function assertCanReadConfigurations(req: Request, companyId: string) { @@ -1303,7 +1507,10 @@ export function agentRoutes( resource: { type: "company", companyId }, }); if (!decision.allowed) { - throw forbidden(decision.explanation, authorizationDeniedDetails(decision)); + throw forbidden( + decision.explanation, + authorizationDeniedDetails(decision), + ); } return req.actor.agentId ? await svc.getById(req.actor.agentId) : null; } @@ -1311,7 +1518,12 @@ export function agentRoutes( } async function getAccessibleAgent(req: Request, res: Response, id: string) { - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!agent) return null; if (req.actor.type === "board") { await assertBoardCanManageAgentsForCompany(req, agent.companyId); @@ -1319,7 +1531,10 @@ export function agentRoutes( return agent; } - async function actorCanReadConfigurationsForCompany(req: Request, companyId: string) { + async function actorCanReadConfigurationsForCompany( + req: Request, + companyId: string, + ) { // Mirrors assertCanReadConfigurations but returns a boolean instead of // throwing. Board actors only need company access; agent actors must pass // the agent configuration read grant ladder so peer agents cannot snoop @@ -1342,7 +1557,10 @@ export function agentRoutes( agent: NonNullable>>, payload: Record | null | undefined, ) { - const issueId = typeof payload?.issueId === "string" && payload.issueId.trim() ? payload.issueId : null; + const issueId = + typeof payload?.issueId === "string" && payload.issueId.trim() + ? payload.issueId + : null; if (!issueId) { return { status: "skipped" as const, @@ -1361,7 +1579,12 @@ export function agentRoutes( executionRunId: issuesTable.executionRunId, }) .from(issuesTable) - .where(and(eq(issuesTable.id, issueId), eq(issuesTable.companyId, agent.companyId))) + .where( + and( + eq(issuesTable.id, issueId), + eq(issuesTable.companyId, agent.companyId), + ), + ) .then((rows) => rows[0] ?? null); if (!issue?.executionRunId) { @@ -1377,7 +1600,10 @@ export function agentRoutes( } const executionRun = await heartbeat.getRun(issue.executionRunId); - if (!executionRun || (executionRun.status !== "queued" && executionRun.status !== "running")) { + if ( + !executionRun || + (executionRun.status !== "queued" && executionRun.status !== "running") + ) { return { status: "skipped" as const, reason: "wakeup_skipped", @@ -1405,7 +1631,10 @@ export function agentRoutes( }; } - async function assertCanUpdateAgent(req: Request, targetAgent: { id: string; companyId: string }) { + async function assertCanUpdateAgent( + req: Request, + targetAgent: { id: string; companyId: string }, + ) { if (!hasCompanyAccess(req, targetAgent.companyId)) { throw notFound("Agent not found"); } @@ -1413,13 +1642,20 @@ export function agentRoutes( const decision = await access.decide({ actor: req.actor, action: "agent_config:update", - resource: { type: "agent", companyId: targetAgent.companyId, agentId: targetAgent.id }, + resource: { + type: "agent", + companyId: targetAgent.companyId, + agentId: targetAgent.id, + }, }); if (decision.allowed) return; throw forbidden(decision.explanation, authorizationDeniedDetails(decision)); } - async function assertCanReadAgent(req: Request, targetAgent: { id: string; companyId: string }) { + async function assertCanReadAgent( + req: Request, + targetAgent: { id: string; companyId: string }, + ) { if (!hasCompanyAccess(req, targetAgent.companyId)) { throw notFound("Agent not found"); } @@ -1437,7 +1673,11 @@ export function agentRoutes( const decision = await access.decide({ actor: req.actor, action: "agent_config:read", - resource: { type: "agent", companyId: targetAgent.companyId, agentId: targetAgent.id }, + resource: { + type: "agent", + companyId: targetAgent.companyId, + agentId: targetAgent.id, + }, }); if (decision.allowed) return; @@ -1472,7 +1712,9 @@ export function agentRoutes( * (listEnabledServerAdapters documents the same rule: hidden from selection, * still functional for agents that already use them). */ - function assertSelectableAdapterType(type: string | null | undefined): string { + function assertSelectableAdapterType( + type: string | null | undefined, + ): string { const adapterType = assertKnownAdapterType(type); const disabled = new Set(getDisabledAdapterTypes()); if (!disabled.has(adapterType)) return adapterType; @@ -1481,8 +1723,8 @@ export function agentRoutes( .filter((t) => !disabled.has(t)) .sort(); throw unprocessable( - `Adapter "${adapterType}" is not available on this instance. ` - + `Available adapters: ${available.length > 0 ? available.join(", ") : "(none configured)"}`, + `Adapter "${adapterType}" is not available on this instance. ` + + `Available adapters: ${available.length > 0 ? available.join(", ") : "(none configured)"}`, ); } @@ -1496,20 +1738,30 @@ export function agentRoutes( if (!environment) { throw unprocessable("Selected environment was not found"); } - if (options?.allowedDrivers && !options.allowedDrivers.includes(environment.driver)) { - throw unprocessable(`Environment driver "${environment.driver}" is not allowed here`); + if ( + options?.allowedDrivers && + !options.allowedDrivers.includes(environment.driver) + ) { + throw unprocessable( + `Environment driver "${environment.driver}" is not allowed here`, + ); } if (environment.driver === "sandbox" && options?.allowedSandboxProviders) { - const config = environment.config && typeof environment.config === "object" - ? environment.config as Record - : {}; - const provider = typeof config.provider === "string" ? config.provider : ""; + const config = + environment.config && typeof environment.config === "object" + ? (environment.config as Record) + : {}; + const provider = + typeof config.provider === "string" ? config.provider : ""; if (provider === "fake") { throw unprocessable( `Selected sandbox provider "${provider}" is not supported for agent defaults yet`, ); } - if (options.allowedSandboxProviders.length > 0 && !options.allowedSandboxProviders.includes(provider)) { + if ( + options.allowedSandboxProviders.length > 0 && + !options.allowedSandboxProviders.includes(provider) + ) { throw unprocessable( `Selected sandbox provider "${provider || "unknown"}" is not supported for agent defaults yet`, ); @@ -1525,11 +1777,19 @@ export function agentRoutes( return supportedEnvironmentDriversForAdapter(adapterType); } - function allowedSandboxProvidersForAgent(adapterType: string): string[] | undefined { - return supportedEnvironmentDriversForAdapter(adapterType).includes("sandbox") ? [] : []; + function allowedSandboxProvidersForAgent( + adapterType: string, + ): string[] | undefined { + return supportedEnvironmentDriversForAdapter(adapterType).includes( + "sandbox", + ) + ? [] + : []; } - async function resolveCompanyIdForAgentReference(req: Request): Promise { + async function resolveCompanyIdForAgentReference( + req: Request, + ): Promise { const companyIdQuery = req.query.companyId; const requestedCompanyId = typeof companyIdQuery === "string" && companyIdQuery.trim().length > 0 @@ -1545,18 +1805,25 @@ export function agentRoutes( return null; } - async function normalizeAgentReference(req: Request, rawId: string): Promise { + async function normalizeAgentReference( + req: Request, + rawId: string, + ): Promise { const raw = rawId.trim(); if (isUuidLike(raw)) return raw; const companyId = await resolveCompanyIdForAgentReference(req); if (!companyId) { - throw unprocessable("Agent shortname lookup requires companyId query parameter"); + throw unprocessable( + "Agent shortname lookup requires companyId query parameter", + ); } const resolved = await svc.resolveByReference(companyId, raw); if (resolved.ambiguous) { - throw conflict("Agent shortname is ambiguous in this company. Use the agent ID."); + throw conflict( + "Agent shortname is ambiguous in this company. Use the agent ID.", + ); } if (!resolved.agent) { throw notFound("Agent not found"); @@ -1569,15 +1836,20 @@ export function agentRoutes( sourceIssueIds?: string[]; }): string[] { const values: string[] = []; - if (Array.isArray(input.sourceIssueIds)) values.push(...input.sourceIssueIds); - if (typeof input.sourceIssueId === "string" && input.sourceIssueId.length > 0) { + if (Array.isArray(input.sourceIssueIds)) + values.push(...input.sourceIssueIds); + if ( + typeof input.sourceIssueId === "string" && + input.sourceIssueId.length > 0 + ) { values.push(input.sourceIssueId); } return Array.from(new Set(values)); } function asRecord(value: unknown): Record | null { - if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + if (typeof value !== "object" || value === null || Array.isArray(value)) + return null; return value as Record; } @@ -1606,7 +1878,10 @@ export function agentRoutes( const merged = { ...nextAdapterConfig }; for (const key of KNOWN_INSTRUCTIONS_BUNDLE_KEYS) { - if (merged[key] === undefined && existingAdapterConfig[key] !== undefined) { + if ( + merged[key] === undefined && + existingAdapterConfig[key] !== undefined + ) { merged[key] = existingAdapterConfig[key]; } } @@ -1622,10 +1897,20 @@ export function agentRoutes( } if (typeof value !== "string") return null; const normalized = value.trim().toLowerCase(); - if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") { + if ( + normalized === "true" || + normalized === "1" || + normalized === "yes" || + normalized === "on" + ) { return true; } - if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") { + if ( + normalized === "false" || + normalized === "0" || + normalized === "no" || + normalized === "off" + ) { return false; } return null; @@ -1665,7 +1950,9 @@ export function agentRoutes( runtimeConfig: unknown, ): Promise> { const parsedRuntimeConfig = asRecord(runtimeConfig); - const normalizedRuntimeConfig = parsedRuntimeConfig ? { ...parsedRuntimeConfig } : {}; + const normalizedRuntimeConfig = parsedRuntimeConfig + ? { ...parsedRuntimeConfig } + : {}; const parsedHeartbeat = asRecord(normalizedRuntimeConfig.heartbeat); const heartbeat = parsedHeartbeat ? { ...parsedHeartbeat } : {}; @@ -1681,7 +1968,8 @@ export function agentRoutes( const parsedModelProfiles = asRecord(normalizedRuntimeConfig.modelProfiles); const modelProfiles = parsedModelProfiles ? { ...parsedModelProfiles } : {}; if (!Object.prototype.hasOwnProperty.call(modelProfiles, "cheap")) { - const adapterModelProfiles = await listNewAgentAdapterModelProfiles(adapterType); + const adapterModelProfiles = + await listNewAgentAdapterModelProfiles(adapterType); if (adapterModelProfiles.some((profile) => profile.key === "cheap")) { modelProfiles.cheap = { enabled: false }; } @@ -1693,7 +1981,9 @@ export function agentRoutes( return normalizedRuntimeConfig; } - function listRuntimeModelProfileAdapterConfigs(runtimeConfig: unknown): Array<{ + function listRuntimeModelProfileAdapterConfigs( + runtimeConfig: unknown, + ): Array<{ profileKey: string; profile: Record; adapterConfig: Record; @@ -1723,7 +2013,10 @@ export function agentRoutes( return entries; } - function assertNoAgentRuntimeConfigAdapterConfigMutation(req: Request, runtimeConfig: unknown) { + function assertNoAgentRuntimeConfigAdapterConfigMutation( + req: Request, + runtimeConfig: unknown, + ) { for (const entry of listRuntimeModelProfileAdapterConfigs(runtimeConfig)) { assertNoAgentAdapterConfigMutation(req, entry.adapterConfig, entry.path); } @@ -1735,14 +2028,15 @@ export function agentRoutes( adapterConfig: Record; constraintAdapterConfig?: Record; }): Promise> { - const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence( - input.companyId, - input.adapterConfig, - { - strictMode: strictSecretsMode, - adapterType: input.adapterType ?? null, - }, - ); + const normalizedAdapterConfig = + await secretsSvc.normalizeAdapterConfigForPersistence( + input.companyId, + input.adapterConfig, + { + strictMode: strictSecretsMode, + adapterType: input.adapterType ?? null, + }, + ); await assertAdapterConfigConstraints( input.adapterType, input.constraintAdapterConfig @@ -1760,7 +2054,8 @@ export function agentRoutes( ): Promise> { const entries = listRuntimeModelProfileAdapterConfigs(runtimeConfig); if (entries.length === 0) return runtimeConfig; - const adapterModelProfiles = await listNewAgentAdapterModelProfiles(adapterType); + const adapterModelProfiles = + await listNewAgentAdapterModelProfiles(adapterType); const normalizedRuntimeConfig = { ...runtimeConfig }; const modelProfiles = asRecord(runtimeConfig.modelProfiles) ?? {}; @@ -1768,17 +2063,21 @@ export function agentRoutes( normalizedRuntimeConfig.modelProfiles = normalizedModelProfiles; for (const entry of entries) { - const adapterProfile = adapterModelProfiles.find((profile) => profile.key === entry.profileKey); - const adapterDefaultConfig = asRecord(adapterProfile?.adapterConfig) ?? {}; - const normalizedAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({ - companyId, - adapterType, - adapterConfig: entry.adapterConfig, - constraintAdapterConfig: { - ...baseAdapterConfig, - ...adapterDefaultConfig, - }, - }); + const adapterProfile = adapterModelProfiles.find( + (profile) => profile.key === entry.profileKey, + ); + const adapterDefaultConfig = + asRecord(adapterProfile?.adapterConfig) ?? {}; + const normalizedAdapterConfig = + await normalizeMediatedAdapterConfigForPersistence({ + companyId, + adapterType, + adapterConfig: entry.adapterConfig, + constraintAdapterConfig: { + ...baseAdapterConfig, + ...adapterDefaultConfig, + }, + }); normalizedModelProfiles[entry.profileKey] = { ...entry.profile, adapterConfig: normalizedAdapterConfig, @@ -1798,19 +2097,32 @@ export function agentRoutes( adapterConfig: Record, ): Record { if (adapterType !== "openclaw_gateway") return adapterConfig; - const disableDeviceAuth = parseBooleanLike(adapterConfig.disableDeviceAuth) === true; + const disableDeviceAuth = + parseBooleanLike(adapterConfig.disableDeviceAuth) === true; if (disableDeviceAuth) return adapterConfig; - if (asNonEmptyString(adapterConfig.devicePrivateKeyPem)) return adapterConfig; - return { ...adapterConfig, devicePrivateKeyPem: generateEd25519PrivateKeyPem() }; + if (asNonEmptyString(adapterConfig.devicePrivateKeyPem)) + return adapterConfig; + return { + ...adapterConfig, + devicePrivateKeyPem: generateEd25519PrivateKeyPem(), + }; } function codexLocalAgentHome(companyId: string, agentId: string): string { const instanceRoot = resolvePaperclipInstanceRootForAdapter({ homeDir: asNonEmptyString(process.env.PAPERCLIP_HOME) ?? undefined, - instanceId: asNonEmptyString(process.env.PAPERCLIP_INSTANCE_ID) ?? undefined, + instanceId: + asNonEmptyString(process.env.PAPERCLIP_INSTANCE_ID) ?? undefined, env: process.env, }); - return path.resolve(instanceRoot, "companies", companyId, "agents", agentId, "codex-home"); + return path.resolve( + instanceRoot, + "companies", + companyId, + "agents", + agentId, + "codex-home", + ); } function codexLocalEnvKeyConfigured(value: unknown): boolean { @@ -1834,11 +2146,16 @@ export function agentRoutes( if (adapterType !== "codex_local") return adapterConfig; const existingEnv = asRecord(adapterConfig.env); if (!existingEnv) return adapterConfig; - if (!codexLocalEnvKeyConfigured(existingEnv.OPENAI_API_KEY)) return adapterConfig; - if (codexLocalEnvKeyConfigured(existingEnv.CODEX_HOME)) return adapterConfig; + if (!codexLocalEnvKeyConfigured(existingEnv.OPENAI_API_KEY)) + return adapterConfig; + if (codexLocalEnvKeyConfigured(existingEnv.CODEX_HOME)) + return adapterConfig; return { ...adapterConfig, - env: { ...existingEnv, CODEX_HOME: codexLocalAgentHome(companyId, agentId) }, + env: { + ...existingEnv, + CODEX_HOME: codexLocalAgentHome(companyId, agentId), + }, }; } @@ -1852,7 +2169,8 @@ export function agentRoutes( typeof next.dangerouslyBypassApprovalsAndSandbox === "boolean" || typeof next.dangerouslyBypassSandbox === "boolean"; if (!hasBypassFlag) { - next.dangerouslyBypassApprovalsAndSandbox = DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX; + next.dangerouslyBypassApprovalsAndSandbox = + DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX; } return ensureGatewayDeviceKey(adapterType, next); } @@ -1883,7 +2201,10 @@ export function agentRoutes( } } - function resolveInstructionsFilePath(candidatePath: string, adapterConfig: Record) { + function resolveInstructionsFilePath( + candidatePath: string, + adapterConfig: Record, + ) { const trimmed = candidatePath.trim(); if (path.isAbsolute(trimmed)) return trimmed; @@ -1894,19 +2215,23 @@ export function agentRoutes( ); } if (!path.isAbsolute(cwd)) { - throw unprocessable("adapterConfig.cwd must be an absolute path to resolve relative instructions path"); + throw unprocessable( + "adapterConfig.cwd must be an absolute path to resolve relative instructions path", + ); } return path.resolve(cwd, trimmed); } - async function materializeDefaultInstructionsBundleForNewAgent( + async function materializeDefaultInstructionsBundleForNewAgent< + T extends { + id: string; + companyId: string; + name: string; + role: string; + adapterType: string; + adapterConfig: unknown; + }, + >( agent: T, input?: { files: Record; entryFile?: string }, ): Promise { @@ -1916,28 +2241,43 @@ export function agentRoutes( const adapterConfig = asRecord(agent.adapterConfig) ?? {}; const hasExplicitInstructionsBundle = - Boolean(asNonEmptyString(adapterConfig.instructionsBundleMode)) - || Boolean(asNonEmptyString(adapterConfig.instructionsRootPath)) - || Boolean(asNonEmptyString(adapterConfig.instructionsEntryFile)) - || Boolean(asNonEmptyString(adapterConfig.instructionsFilePath)) - || Boolean(asNonEmptyString(adapterConfig.agentsMdPath)); + Boolean(asNonEmptyString(adapterConfig.instructionsBundleMode)) || + Boolean(asNonEmptyString(adapterConfig.instructionsRootPath)) || + Boolean(asNonEmptyString(adapterConfig.instructionsEntryFile)) || + Boolean(asNonEmptyString(adapterConfig.instructionsFilePath)) || + Boolean(asNonEmptyString(adapterConfig.agentsMdPath)); if (hasExplicitInstructionsBundle) { const nextAdapterConfig = { ...adapterConfig }; const hadLegacyPrompt = - Object.prototype.hasOwnProperty.call(nextAdapterConfig, "promptTemplate") - || Object.prototype.hasOwnProperty.call(nextAdapterConfig, "bootstrapPromptTemplate"); + Object.prototype.hasOwnProperty.call( + nextAdapterConfig, + "promptTemplate", + ) || + Object.prototype.hasOwnProperty.call( + nextAdapterConfig, + "bootstrapPromptTemplate", + ); delete nextAdapterConfig.promptTemplate; delete nextAdapterConfig.bootstrapPromptTemplate; if (!hadLegacyPrompt) return agent; - const updated = await svc.update(agent.id, { adapterConfig: nextAdapterConfig }, { - allowPendingApprovalConfigUpdate: true, - }); - return (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig }; + const updated = await svc.update( + agent.id, + { adapterConfig: nextAdapterConfig }, + { + allowPendingApprovalConfigUpdate: true, + }, + ); + return ( + (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig } + ); } - const files = input?.files - ?? await loadDefaultAgentInstructionsBundle(resolveDefaultAgentInstructionsBundleRole(agent.role)); + const files = + input?.files ?? + (await loadDefaultAgentInstructionsBundle( + resolveDefaultAgentInstructionsBundleRole(agent.role), + )); const materialized = await instructions.materializeManagedBundle( agent, files, @@ -1947,17 +2287,29 @@ export function agentRoutes( delete nextAdapterConfig.promptTemplate; delete nextAdapterConfig.bootstrapPromptTemplate; - const updated = await svc.update(agent.id, { adapterConfig: nextAdapterConfig }, { - allowPendingApprovalConfigUpdate: true, - }); - return (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig }; + const updated = await svc.update( + agent.id, + { adapterConfig: nextAdapterConfig }, + { + allowPendingApprovalConfigUpdate: true, + }, + ); + return ( + (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig } + ); } - function assertNoNewAgentLegacyPromptTemplate(adapterType: string, adapterConfig: Record) { + function assertNoNewAgentLegacyPromptTemplate( + adapterType: string, + adapterConfig: Record, + ) { if (!adapterSupportsInstructionsBundle(adapterType)) return; if ( - Object.prototype.hasOwnProperty.call(adapterConfig, "promptTemplate") - || Object.prototype.hasOwnProperty.call(adapterConfig, "bootstrapPromptTemplate") + Object.prototype.hasOwnProperty.call(adapterConfig, "promptTemplate") || + Object.prototype.hasOwnProperty.call( + adapterConfig, + "bootstrapPromptTemplate", + ) ) { throw unprocessable( "New agents must use instructionsBundle/AGENTS.md instead of adapterConfig.promptTemplate or bootstrapPromptTemplate", @@ -1978,14 +2330,22 @@ export function agentRoutes( const decision = await access.decide({ actor: req.actor, action: "agent_config:update", - resource: { type: "agent", companyId: targetAgent.companyId, agentId: targetAgent.id }, + resource: { + type: "agent", + companyId: targetAgent.companyId, + agentId: targetAgent.id, + }, scope: changeScope, }); if (decision.allowed) { return; } - if (decision.reason === "deny_missing_consent" && req.actor.type === "agent" && targetKeys.length > 0) { + if ( + decision.reason === "deny_missing_consent" && + req.actor.type === "agent" && + targetKeys.length > 0 + ) { try { await changeConsentGateService(db).assertConsented({ companyId: targetAgent.companyId, @@ -1995,7 +2355,10 @@ export function agentRoutes( }); } catch (err) { if (err instanceof HttpError && err.status === 403) { - throw forbidden(decision.explanation, authorizationDeniedDetails(decision)); + throw forbidden( + decision.explanation, + authorizationDeniedDetails(decision), + ); } throw err; } @@ -2003,35 +2366,41 @@ export function agentRoutes( const consentedDecision = await access.decide({ actor: req.actor, action: "agent_config:update", - resource: { type: "agent", companyId: targetAgent.companyId, agentId: targetAgent.id }, + resource: { + type: "agent", + companyId: targetAgent.companyId, + agentId: targetAgent.id, + }, scope: { ...changeScope, consentedChange: true }, }); if (consentedDecision.allowed) { return; } - throw forbidden(consentedDecision.explanation, authorizationDeniedDetails(consentedDecision)); + throw forbidden( + consentedDecision.explanation, + authorizationDeniedDetails(consentedDecision), + ); } throw forbidden(decision.explanation, authorizationDeniedDetails(decision)); } - async function assertCanManageInstructionsPath(req: Request, targetAgent: { id: string; companyId: string }) { - await assertCanApplyProtectedAgentChange( - req, - targetAgent, - [agentInstructionsChangeTargetKey(targetAgent.id)], - ); + async function assertCanManageInstructionsPath( + req: Request, + targetAgent: { id: string; companyId: string }, + ) { + await assertCanApplyProtectedAgentChange(req, targetAgent, [ + agentInstructionsChangeTargetKey(targetAgent.id), + ]); } async function assertCanApplyAgentProfileChange( req: Request, targetAgent: { id: string; companyId: string }, ) { - await assertCanApplyProtectedAgentChange( - req, - targetAgent, - [agentProfileChangeTargetKey(targetAgent.id)], - ); + await assertCanApplyProtectedAgentChange(req, targetAgent, [ + agentProfileChangeTargetKey(targetAgent.id), + ]); } function assertNoAgentInstructionsConfigMutation( @@ -2040,17 +2409,21 @@ export function agentRoutes( path = "adapterConfig", ) { if (req.actor.type !== "agent" || !adapterConfig) return; - const changedSensitiveKeys = KNOWN_INSTRUCTIONS_BUNDLE_KEYS - .filter((key) => adapterConfig[key] !== undefined) - .map((key) => `${path}.${key}`); + const changedSensitiveKeys = KNOWN_INSTRUCTIONS_BUNDLE_KEYS.filter( + (key) => adapterConfig[key] !== undefined, + ).map((key) => `${path}.${key}`); if (changedSensitiveKeys.length === 0) return; throw forbidden( `Agent-authenticated callers cannot modify instructions path or bundle configuration (${changedSensitiveKeys.join(", ")})`, ); } - function adapterConfigTouchesInstructionsConfig(adapterConfig: Record) { - return KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some((key) => adapterConfig[key] !== undefined); + function adapterConfigTouchesInstructionsConfig( + adapterConfig: Record, + ) { + return KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some( + (key) => adapterConfig[key] !== undefined, + ); } function assertNoAgentAdapterConfigMutation( @@ -2104,9 +2477,10 @@ export function agentRoutes( if (!requestedDesiredSkills) return undefined; const out = new Map(); for (const value of requestedDesiredSkills) { - const entry = typeof value === "string" - ? { key: value.trim(), versionId: null } - : { key: value.key.trim(), versionId: value.versionId ?? null }; + const entry = + typeof value === "string" + ? { key: value.trim(), versionId: null } + : { key: value.key.trim(), versionId: value.versionId ?? null }; if (!entry.key || out.has(entry.key)) continue; out.set(entry.key, entry); } @@ -2139,14 +2513,22 @@ export function agentRoutes( } = {}, ) { const preference = readPaperclipSkillSyncPreference(config); - const betaSkillsEnabled = (await instanceSettings.getExperimental()).enableBetaSkills === true; - const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(companyId, { - materializeMissing: options.materializeMissing - ?? shouldMaterializeRuntimeSkillsForAdapter(adapterType), - versionSelections: skillVersionSelectionMap(preference.desiredSkillEntries, { - versionPinsEnabled: betaSkillsEnabled, - }), - }); + const betaSkillsEnabled = + (await instanceSettings.getExperimental()).enableBetaSkills === true; + const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries( + companyId, + { + materializeMissing: + options.materializeMissing ?? + shouldMaterializeRuntimeSkillsForAdapter(adapterType), + versionSelections: skillVersionSelectionMap( + preference.desiredSkillEntries, + { + versionPinsEnabled: betaSkillsEnabled, + }, + ), + }, + ); return { ...config, paperclipRuntimeSkills: runtimeSkillEntries, @@ -2166,45 +2548,65 @@ export function agentRoutes( adapterConfig, desiredSkills: null as string[] | null, desiredSkillEntries: null as AgentDesiredSkillEntry[] | null, - runtimeSkillEntries: null as Awaited> | null, + runtimeSkillEntries: null as Awaited< + ReturnType + > | null, }; } if (requestedDesiredSkills.some((entry) => entry.versionId !== null)) { - const betaSkillsEnabled = (await instanceSettings.getExperimental()).enableBetaSkills === true; + const betaSkillsEnabled = + (await instanceSettings.getExperimental()).enableBetaSkills === true; if (!betaSkillsEnabled) { - throw badRequest("Beta skill version pins require the Beta skills experimental setting to be enabled."); + throw badRequest( + "Beta skill version pins require the Beta skills experimental setting to be enabled.", + ); } } - const { resolved: resolvedRequestedSkillEntries, unresolved: unresolvedDesiredSkillKeys } = - await companySkills.resolveRequestedSkillEntries(companyId, requestedDesiredSkills, { + const { + resolved: resolvedRequestedSkillEntries, + unresolved: unresolvedDesiredSkillKeys, + } = await companySkills.resolveRequestedSkillEntries( + companyId, + requestedDesiredSkills, + { tolerateUnknownReferences: options.tolerateUnknownDesiredSkills, - }); + }, + ); const requestedSkillEntries = [ ...resolvedRequestedSkillEntries, ...unresolvedDesiredSkillKeys.map((key) => ({ key, versionId: null })), ].filter( - (entry, index, entries) => entries.findIndex((candidate) => candidate.key === entry.key) === index, + (entry, index, entries) => + entries.findIndex((candidate) => candidate.key === entry.key) === index, ); const currentPreference = readPaperclipSkillSyncPreference(adapterConfig); - const { resolved: resolvedCurrentSkillEntries, unresolved: unresolvedCurrentSkillKeys } = + const { + resolved: resolvedCurrentSkillEntries, + unresolved: unresolvedCurrentSkillKeys, + } = currentPreference.desiredSkillEntries.length > 0 ? await companySkills.resolveRequestedSkillEntries( - companyId, - currentPreference.desiredSkillEntries, - { tolerateUnknownReferences: true }, - ) + companyId, + currentPreference.desiredSkillEntries, + { tolerateUnknownReferences: true }, + ) : { resolved: [], unresolved: [] }; const currentSkillEntries = [ ...resolvedCurrentSkillEntries, ...unresolvedCurrentSkillKeys.map((key) => ({ key, versionId: null })), ].filter( - (entry, index, entries) => entries.findIndex((candidate) => candidate.key === entry.key) === index, + (entry, index, entries) => + entries.findIndex((candidate) => candidate.key === entry.key) === index, ); - const desiredSkillEntries = mergeDesiredSkillEntries(currentSkillEntries, requestedSkillEntries, mode); + const desiredSkillEntries = mergeDesiredSkillEntries( + currentSkillEntries, + requestedSkillEntries, + mode, + ); const desiredSkills = desiredSkillEntries.map((entry) => entry.key); const resolvedKeys = new Set([ ...resolvedCurrentSkillEntries.map((entry) => entry.key), @@ -2213,22 +2615,31 @@ export function agentRoutes( // Runtime materialization + version selection only ever consider final // assignments that resolve to the company library; stale keys remain // persisted and explicitly removable without reaching adapter runtimes. - const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(companyId, { - materializeMissing: shouldMaterializeRuntimeSkillsForAdapter(adapterType), - versionSelections: skillVersionSelectionMap( - desiredSkillEntries.filter((entry) => resolvedKeys.has(entry.key)), - ), - }); + const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries( + companyId, + { + materializeMissing: + shouldMaterializeRuntimeSkillsForAdapter(adapterType), + versionSelections: skillVersionSelectionMap( + desiredSkillEntries.filter((entry) => resolvedKeys.has(entry.key)), + ), + }, + ); return { - adapterConfig: writePaperclipSkillSyncPreference(adapterConfig, desiredSkillEntries), + adapterConfig: writePaperclipSkillSyncPreference( + adapterConfig, + desiredSkillEntries, + ), desiredSkills, desiredSkillEntries, runtimeSkillEntries, }; } - function redactForRestrictedAgentView(agent: Awaited>) { + function redactForRestrictedAgentView( + agent: Awaited>, + ) { if (!agent) return null; return { ...agent, @@ -2237,7 +2648,9 @@ export function agentRoutes( }; } - function redactAgentConfiguration(agent: Awaited>) { + function redactAgentConfiguration( + agent: Awaited>, + ) { if (!agent) return null; return { id: agent.id, @@ -2256,29 +2669,35 @@ export function agentRoutes( } function redactRevisionSnapshot(snapshot: unknown): Record { - if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return {}; + if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) + return {}; const record = snapshot as Record; return { ...record, adapterConfig: redactEventPayload( - typeof record.adapterConfig === "object" && record.adapterConfig !== null + typeof record.adapterConfig === "object" && + record.adapterConfig !== null ? (record.adapterConfig as Record) : {}, ), runtimeConfig: redactEventPayload( - typeof record.runtimeConfig === "object" && record.runtimeConfig !== null + typeof record.runtimeConfig === "object" && + record.runtimeConfig !== null ? (record.runtimeConfig as Record) : {}, ), metadata: typeof record.metadata === "object" && record.metadata !== null ? redactEventPayload(record.metadata as Record) - : record.metadata ?? null, + : (record.metadata ?? null), }; } function redactConfigRevision( - revision: Record & { beforeConfig: unknown; afterConfig: unknown }, + revision: Record & { + beforeConfig: unknown; + afterConfig: unknown; + }, ) { return { ...revision, @@ -2287,9 +2706,13 @@ export function agentRoutes( }; } - function toLeanOrgNode(node: Record): Record { + function toLeanOrgNode( + node: Record, + ): Record { const reports = Array.isArray(node.reports) - ? (node.reports as Array>).map((report) => toLeanOrgNode(report)) + ? (node.reports as Array>).map((report) => + toLeanOrgNode(report), + ) : []; return { id: String(node.id), @@ -2309,46 +2732,62 @@ export function agentRoutes( } }); - router.get("/companies/:companyId/adapters/:type/models", async (req, res) => { - const companyId = req.params.companyId as string; - assertCompanyAccess(req, companyId); - const type = assertKnownAdapterType(req.params.type as string); - const refresh = typeof req.query.refresh === "string" - ? ["1", "true", "yes"].includes(req.query.refresh.toLowerCase()) - : false; - const environmentId = asNonEmptyString(req.query.environmentId); - const environment = environmentId ? await environmentsSvc.getById(environmentId) : null; - if (environmentId && !environment) { - res.status(404).json({ error: "Environment not found" }); - return; - } - if (type === "opencode_local" && environment && environment.driver !== "local") { - const adapter = requireServerAdapter(type); - res.json(adapter.models ?? []); - return; - } - const models = refresh - ? await refreshAdapterModels(type) - : await listAdapterModels(type); - res.json(models); - }); + router.get( + "/companies/:companyId/adapters/:type/models", + async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const type = assertKnownAdapterType(req.params.type as string); + const refresh = + typeof req.query.refresh === "string" + ? ["1", "true", "yes"].includes(req.query.refresh.toLowerCase()) + : false; + const environmentId = asNonEmptyString(req.query.environmentId); + const environment = environmentId + ? await environmentsSvc.getById(environmentId) + : null; + if (environmentId && !environment) { + res.status(404).json({ error: "Environment not found" }); + return; + } + if ( + type === "opencode_local" && + environment && + environment.driver !== "local" + ) { + const adapter = requireServerAdapter(type); + res.json(adapter.models ?? []); + return; + } + const models = refresh + ? await refreshAdapterModels(type) + : await listAdapterModels(type); + res.json(models); + }, + ); - router.get("/companies/:companyId/adapters/:type/model-profiles", async (req, res) => { - const companyId = req.params.companyId as string; - assertCompanyAccess(req, companyId); - const type = assertKnownAdapterType(req.params.type as string); - const profiles = await listAdapterModelProfiles(type); - res.json(profiles); - }); + router.get( + "/companies/:companyId/adapters/:type/model-profiles", + async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const type = assertKnownAdapterType(req.params.type as string); + const profiles = await listAdapterModelProfiles(type); + res.json(profiles); + }, + ); - router.get("/companies/:companyId/adapters/:type/detect-model", async (req, res) => { - const companyId = req.params.companyId as string; - assertCompanyAccess(req, companyId); - const type = assertKnownAdapterType(req.params.type as string); + router.get( + "/companies/:companyId/adapters/:type/detect-model", + async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const type = assertKnownAdapterType(req.params.type as string); - const detected = await detectAdapterModel(type); - res.json(detected); - }); + const detected = await detectAdapterModel(type); + res.json(detected); + }, + ); router.post( "/companies/:companyId/adapters/:type/test-environment", @@ -2360,39 +2799,52 @@ export function agentRoutes( const adapter = requireServerAdapter(type); - const inputAdapterConfig = - (req.body?.adapterConfig ?? {}) as Record; + const inputAdapterConfig = (req.body?.adapterConfig ?? {}) as Record< + string, + unknown + >; const requestedEnvironmentId = - typeof req.body?.environmentId === "string" && req.body.environmentId.trim().length > 0 + typeof req.body?.environmentId === "string" && + req.body.environmentId.trim().length > 0 ? (req.body.environmentId as string) : null; - const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence( - companyId, - inputAdapterConfig, - { strictMode: strictSecretsMode, adapterType: type }, - ); + const normalizedAdapterConfig = + await secretsSvc.normalizeAdapterConfigForPersistence( + companyId, + inputAdapterConfig, + { strictMode: strictSecretsMode, adapterType: type }, + ); // Prospective, non-persisted config: resolve the acting user's own user // secrets in owner_scoped mode (no declaration rows exist for this config). // Record an honest audit consumer — environment: when the caller selected // one, otherwise system:adapter_test — never a fake agent consumer. - const { config: runtimeAdapterConfig } = await secretsSvc.resolveAdapterConfigForRuntime( - companyId, - normalizedAdapterConfig, - buildActorSecretContext( - req, - requestedEnvironmentId - ? { consumerType: "environment", consumerId: requestedEnvironmentId } - : { consumerType: "system", consumerId: "adapter_test" }, - ), - { adapterType: type, userSecretMediation: "owner_scoped" }, - ); - - const { executionTarget, environmentName, fallbackChecks, sandboxIdentityCheck, release } = - await resolveAdapterTestExecutionContext({ + const { config: runtimeAdapterConfig } = + await secretsSvc.resolveAdapterConfigForRuntime( companyId, - adapterType: type, - environmentId: requestedEnvironmentId, - }); + normalizedAdapterConfig, + buildActorSecretContext( + req, + requestedEnvironmentId + ? { + consumerType: "environment", + consumerId: requestedEnvironmentId, + } + : { consumerType: "system", consumerId: "adapter_test" }, + ), + { adapterType: type, userSecretMediation: "owner_scoped" }, + ); + + const { + executionTarget, + environmentName, + fallbackChecks, + sandboxIdentityCheck, + release, + } = await resolveAdapterTestExecutionContext({ + companyId, + adapterType: type, + environmentId: requestedEnvironmentId, + }); let releaseStatus: "released" | "failed" = "released"; try { @@ -2404,7 +2856,9 @@ export function agentRoutes( const environmentEnvChecks: AdapterEnvironmentCheck[] = []; let effectiveAdapterConfig = runtimeAdapterConfig; if (requestedEnvironmentId) { - const selectedEnvironment = await environmentsSvc.getById(requestedEnvironmentId); + const selectedEnvironment = await environmentsSvc.getById( + requestedEnvironmentId, + ); const environmentEnv = Object.fromEntries( Object.entries(parseObject(selectedEnvironment?.envVars)).filter( ([key]) => !isForbiddenConfigEnvKey(key), @@ -2423,7 +2877,9 @@ export function agentRoutes( environmentSecretContext, ) : []; - const missingKeys = new Set(missingBindings.map((binding) => binding.envKey)); + const missingKeys = new Set( + missingBindings.map((binding) => binding.envKey), + ); if (missingKeys.size > 0) { environmentEnvChecks.push({ code: "environment_env_binding_missing", @@ -2433,13 +2889,16 @@ export function agentRoutes( }); } const resolvableEnvironmentEnv = Object.fromEntries( - Object.entries(environmentEnv).filter(([key]) => !missingKeys.has(key)), - ); - const environmentEnvResolution = await secretsSvc.resolveEnvBindings( - companyId, - resolvableEnvironmentEnv, - environmentSecretContext, + Object.entries(environmentEnv).filter( + ([key]) => !missingKeys.has(key), + ), ); + const environmentEnvResolution = + await secretsSvc.resolveEnvBindings( + companyId, + resolvableEnvironmentEnv, + environmentSecretContext, + ); if (Object.keys(environmentEnvResolution.env).length > 0) { effectiveAdapterConfig = { ...runtimeAdapterConfig, @@ -2455,13 +2914,18 @@ export function agentRoutes( // If the caller explicitly selected an environment, never fall back to // probing the host when we couldn't resolve that environment's // execution target. Surface the diagnostic checks instead. - if (requestedEnvironmentId && !executionTarget && fallbackChecks.length > 0) { + if ( + requestedEnvironmentId && + !executionTarget && + fallbackChecks.length > 0 + ) { const combinedChecks = [...fallbackChecks, ...environmentEnvChecks]; - const status: AdapterEnvironmentTestResult["status"] = combinedChecks.some((c) => c.level === "error") - ? "fail" - : combinedChecks.some((c) => c.level === "warn") - ? "warn" - : "pass"; + const status: AdapterEnvironmentTestResult["status"] = + combinedChecks.some((c) => c.level === "error") + ? "fail" + : combinedChecks.some((c) => c.level === "warn") + ? "warn" + : "pass"; if (status === "fail") releaseStatus = "failed"; const synthesized: AdapterEnvironmentTestResult = { adapterType: type, @@ -2488,12 +2952,17 @@ export function agentRoutes( // A missing environment secret binding blocks real dispatch // (ConfigurationIncompleteFailure in the heartbeat), so the test // reports fail even when the adapter probe itself passed. - const status = environmentEnvChecks.some((c) => c.level === "error") ? "fail" : result.status; + const status = environmentEnvChecks.some((c) => c.level === "error") + ? "fail" + : result.status; if (status === "fail") releaseStatus = "failed"; res.json({ ...result, status, - checks: prefixChecks.length > 0 ? [...prefixChecks, ...result.checks] : result.checks, + checks: + prefixChecks.length > 0 + ? [...prefixChecks, ...result.checks] + : result.checks, }); } catch (err) { releaseStatus = "failed"; @@ -2528,7 +2997,8 @@ export function agentRoutes( requestSchema: startAdapterAuthSessionRequestSchema, invalidRequestError: "The device login start request is invalid.", requestOverrides: { adapterType: type }, - assertSandbox: (data) => assertSandboxLoginEnvironment(companyId, data.environmentId), + assertSandbox: (data) => + assertSandboxLoginEnvironment(companyId, data.environmentId), }); if (!resolved) return; const { ownerUserId: startedByUserId, data } = resolved; @@ -2579,7 +3049,12 @@ export function agentRoutes( const ownerUserId = await assertCanManageAdapterLogin(req, companyId); assertStreamedExecLoginAdapter(type); - const owner = await readOwnerLoginSession(companyId, type, sessionId, ownerUserId); + const owner = await readOwnerLoginSession( + companyId, + type, + sessionId, + ownerUserId, + ); if (!owner) { res.status(404).json({ error: "Adapter login session not found" }); return; @@ -2601,7 +3076,12 @@ export function agentRoutes( // Scope the cancel to this company, adapter, and owner. A non-owner and a // cross-company caller both receive a 404 and cannot cancel a session. - const owner = await readOwnerLoginSession(companyId, type, sessionId, ownerUserId); + const owner = await readOwnerLoginSession( + companyId, + type, + sessionId, + ownerUserId, + ); if (!owner) { res.status(404).json({ error: "Adapter login session not found" }); return; @@ -2610,7 +3090,11 @@ export function agentRoutes( // even when this process does not own the in-flight run, so a cross-process // cancel or a cancel after a restart does not leave the slot held until the // expiry. The reaper deletes the sandbox and finalizes the terminal. - const cancelled = await adapterLoginService.cancelOwnerSession(sessionId, companyId, ownerUserId); + const cancelled = await adapterLoginService.cancelOwnerSession( + sessionId, + companyId, + ownerUserId, + ); // Abort the in-flight run this process owns, so the local login stops at // once instead of waiting for the reaper. A run in another process, or an // already-terminal run, has no controller here. @@ -2634,18 +3118,26 @@ export function agentRoutes( agent.adapterConfig as Record, ); const desiredSkillEntries = preference.desiredSkillEntries.filter( - (entry, index, entries) => entries.findIndex((candidate) => candidate.key === entry.key) === index, + (entry, index, entries) => + entries.findIndex((candidate) => candidate.key === entry.key) === + index, + ); + res.json( + buildUnsupportedSkillSnapshot(agent.adapterType, desiredSkillEntries), ); - res.json(buildUnsupportedSkillSnapshot(agent.adapterType, desiredSkillEntries)); return; } - const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime( - agent.companyId, - agent.adapterConfig, - buildActorSecretContext(req, { consumerType: "agent", consumerId: agent.id }), - { adapterType: agent.adapterType, skipUserSecrets: true }, - ); + const { config: runtimeConfig } = + await secretsSvc.resolveAdapterConfigForRuntime( + agent.companyId, + agent.adapterConfig, + buildActorSecretContext(req, { + consumerType: "agent", + consumerId: agent.id, + }), + { adapterType: agent.adapterType, skipUserSecrets: true }, + ); const runtimeSkillConfig = await buildRuntimeSkillConfig( agent.companyId, agent.adapterType, @@ -2667,11 +3159,18 @@ export function agentRoutes( validate(agentSkillSyncSchema), async (req, res) => { const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!agent) return; await assertCanUpdateAgent(req, agent); - const requestedSkills = normalizeDesiredSkillSelections(req.body.desiredSkills); + const requestedSkills = normalizeDesiredSkillSelections( + req.body.desiredSkills, + ); const { adapterConfig: nextAdapterConfig, desiredSkills, @@ -2692,38 +3191,49 @@ export function agentRoutes( throw unprocessable("Skill sync requires desiredSkills."); } const actor = getActorInfo(req); - const updated = await svc.update(agent.id, { - adapterConfig: nextAdapterConfig, - }, { - recordRevision: { - createdByAgentId: actor.agentId, - createdByUserId: actor.actorType === "user" ? actor.actorId : null, - source: "skill-sync", + const updated = await svc.update( + agent.id, + { + adapterConfig: nextAdapterConfig, }, - }); + { + recordRevision: { + createdByAgentId: actor.agentId, + createdByUserId: actor.actorType === "user" ? actor.actorId : null, + source: "skill-sync", + }, + }, + ); if (!updated) { res.status(404).json({ error: "Agent not found" }); return; } const adapter = findActiveServerAdapter(updated.adapterType); - const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime( - updated.companyId, - updated.adapterConfig, - buildActorSecretContext(req, { consumerType: "agent", consumerId: updated.id }), - { adapterType: updated.adapterType, skipUserSecrets: true }, - ); + const { config: runtimeConfig } = + await secretsSvc.resolveAdapterConfigForRuntime( + updated.companyId, + updated.adapterConfig, + buildActorSecretContext(req, { + consumerType: "agent", + consumerId: updated.id, + }), + { adapterType: updated.adapterType, skipUserSecrets: true }, + ); const runtimeSkillConfig = { ...runtimeConfig, paperclipRuntimeSkills: runtimeSkillEntries, }; const snapshot = adapter?.syncSkills - ? await adapter.syncSkills({ - agentId: updated.id, - companyId: updated.companyId, - adapterType: updated.adapterType, - config: runtimeSkillConfig, - }, desiredSkills) + ? await adapter.syncSkills( + { + agentId: updated.id, + companyId: updated.companyId, + adapterType: updated.adapterType, + config: runtimeSkillConfig, + }, + desiredSkills, + ) : adapter?.listSkills ? await adapter.listSkills({ agentId: updated.id, @@ -2731,7 +3241,10 @@ export function agentRoutes( adapterType: updated.adapterType, config: runtimeSkillConfig, }) - : buildUnsupportedSkillSnapshot(updated.adapterType, desiredSkillEntries); + : buildUnsupportedSkillSnapshot( + updated.adapterType, + desiredSkillEntries, + ); await logActivity(db, { companyId: updated.companyId, @@ -2770,7 +3283,10 @@ export function agentRoutes( return; } const result = await filterAgentsForActor(req, await svc.list(companyId)); - const canReadConfigs = await actorCanReadConfigurationsForCompany(req, companyId); + const canReadConfigs = await actorCanReadConfigurationsForCompany( + req, + companyId, + ); if (canReadConfigs) { res.json(result); return; @@ -2820,14 +3336,16 @@ export function agentRoutes( adapterType: row.adapterType, intervalSec: policy.intervalSec, heartbeatEnabled: policy.enabled, - schedulerActive: statusEligible && policy.enabled && policy.intervalSec > 0, + schedulerActive: + statusEligible && policy.enabled && policy.intervalSec > 0, lastHeartbeatAt: row.lastHeartbeatAt, }; }) - .filter((item) => - item.status !== "paused" && - item.status !== "terminated" && - item.status !== "pending_approval", + .filter( + (item) => + item.status !== "paused" && + item.status !== "terminated" && + item.status !== "pending_approval", ) .sort((left, right) => { if (left.schedulerActive !== right.schedulerActive) { @@ -2844,17 +3362,33 @@ export function agentRoutes( router.get("/companies/:companyId/org", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); - const tree = await filterAgentsForActor(req, await svc.orgForCompany(companyId), companyId); - const leanTree = tree.map((node) => toLeanOrgNode(node as Record)); + const tree = await filterAgentsForActor( + req, + await svc.orgForCompany(companyId), + companyId, + ); + const leanTree = tree.map((node) => + toLeanOrgNode(node as Record), + ); res.json(leanTree); }); router.get("/companies/:companyId/org.svg", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); - const style = (ORG_CHART_STYLES.includes(req.query.style as OrgChartStyle) ? req.query.style : "warmth") as OrgChartStyle; - const tree = await filterAgentsForActor(req, await svc.orgForCompany(companyId), companyId); - const leanTree = tree.map((node) => toLeanOrgNode(node as Record)); + const style = ( + ORG_CHART_STYLES.includes(req.query.style as OrgChartStyle) + ? req.query.style + : "warmth" + ) as OrgChartStyle; + const tree = await filterAgentsForActor( + req, + await svc.orgForCompany(companyId), + companyId, + ); + const leanTree = tree.map((node) => + toLeanOrgNode(node as Record), + ); const svg = renderOrgChartSvg(leanTree as unknown as OrgNode[], style); res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "no-cache"); @@ -2864,10 +3398,23 @@ export function agentRoutes( router.get("/companies/:companyId/org.png", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); - const style = (ORG_CHART_STYLES.includes(req.query.style as OrgChartStyle) ? req.query.style : "warmth") as OrgChartStyle; - const tree = await filterAgentsForActor(req, await svc.orgForCompany(companyId), companyId); - const leanTree = tree.map((node) => toLeanOrgNode(node as Record)); - const png = await renderOrgChartPng(leanTree as unknown as OrgNode[], style); + const style = ( + ORG_CHART_STYLES.includes(req.query.style as OrgChartStyle) + ? req.query.style + : "warmth" + ) as OrgChartStyle; + const tree = await filterAgentsForActor( + req, + await svc.orgForCompany(companyId), + companyId, + ); + const leanTree = tree.map((node) => + toLeanOrgNode(node as Record), + ); + const png = await renderOrgChartPng( + leanTree as unknown as OrgNode[], + style, + ); res.setHeader("Content-Type", "image/png"); res.setHeader("Cache-Control", "no-cache"); res.send(png); @@ -2915,7 +3462,11 @@ export function agentRoutes( }); router.get("/agents/me/inbox-lite", async (req, res) => { - if (req.actor.type !== "agent" || !req.actor.agentId || !req.actor.companyId) { + if ( + req.actor.type !== "agent" || + !req.actor.agentId || + !req.actor.companyId + ) { res.status(401).json({ error: "Agent authentication required" }); return; } @@ -2928,15 +3479,22 @@ export function agentRoutes( includeRoutineExecutions: true, limit: ISSUE_LIST_DEFAULT_LIMIT, }); - const worktreeActivation = await resolveWorktreeRunExecutionActivationState({ - getExperimental: () => instanceSettingsService(db).getExperimental(), - }); - const isWorktreeRuntime = isTruthyRuntimeEnvValue(process.env.PAPERCLIP_IN_WORKTREE); + const worktreeActivation = await resolveWorktreeRunExecutionActivationState( + { + getExperimental: () => instanceSettingsService(db).getExperimental(), + }, + ); + const isWorktreeRuntime = isTruthyRuntimeEnvValue( + process.env.PAPERCLIP_IN_WORKTREE, + ); const eligibleRows = !isWorktreeRuntime ? rows : worktreeActivation.armed - ? rows.filter((issue) => new Date(issue.createdAt) >= new Date(worktreeActivation.cutoff)) - : []; + ? rows.filter( + (issue) => + new Date(issue.createdAt) >= new Date(worktreeActivation.cutoff), + ) + : []; const issueIds = eligibleRows.map((issue) => issue.id); const [dependencyReadiness, recoveryActionByIssue] = await Promise.all([ issuesSvc.listDependencyReadiness(req.actor.companyId, issueIds), @@ -2956,15 +3514,22 @@ export function agentRoutes( updatedAt: issue.updatedAt, activeRun: issue.activeRun, activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null, - dependencyReady: dependencyReadiness.get(issue.id)?.isDependencyReady ?? true, - unresolvedBlockerCount: dependencyReadiness.get(issue.id)?.unresolvedBlockerCount ?? 0, - unresolvedBlockerIssueIds: dependencyReadiness.get(issue.id)?.unresolvedBlockerIssueIds ?? [], + dependencyReady: + dependencyReadiness.get(issue.id)?.isDependencyReady ?? true, + unresolvedBlockerCount: + dependencyReadiness.get(issue.id)?.unresolvedBlockerCount ?? 0, + unresolvedBlockerIssueIds: + dependencyReadiness.get(issue.id)?.unresolvedBlockerIssueIds ?? [], })), ); }); router.get("/agents/me/inbox/mine", async (req, res) => { - if (req.actor.type !== "agent" || !req.actor.agentId || !req.actor.companyId) { + if ( + req.actor.type !== "agent" || + !req.actor.agentId || + !req.actor.companyId + ) { res.status(401).json({ error: "Agent authentication required" }); return; } @@ -2983,7 +3548,12 @@ export function agentRoutes( router.get("/agents/:id", async (req, res) => { const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!agent) return; if (!(await assertAgentReadAllowed(req, res, agent))) return; const isSelf = req.actor.type === "agent" && req.actor.agentId === id; @@ -3048,43 +3618,56 @@ export function agentRoutes( res.json(redactConfigRevision(revision)); }); - router.post("/agents/:id/config-revisions/:revisionId/rollback", async (req, res) => { - const id = req.params.id as string; - const revisionId = req.params.revisionId as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); - if (!existing) return; - await assertCanUpdateAgent(req, existing); + router.post( + "/agents/:id/config-revisions/:revisionId/rollback", + async (req, res) => { + const id = req.params.id as string; + const revisionId = req.params.revisionId as string; + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); + if (!existing) return; + await assertCanUpdateAgent(req, existing); - const actor = getActorInfo(req); - const updated = await svc.rollbackConfigRevision(id, revisionId, { - agentId: actor.agentId, - userId: actor.actorType === "user" ? actor.actorId : null, - }); - if (!updated) { - res.status(404).json({ error: "Revision not found" }); - return; - } + const actor = getActorInfo(req); + const updated = await svc.rollbackConfigRevision(id, revisionId, { + agentId: actor.agentId, + userId: actor.actorType === "user" ? actor.actorId : null, + }); + if (!updated) { + res.status(404).json({ error: "Revision not found" }); + return; + } - await logActivity(db, { - companyId: updated.companyId, - actorType: actor.actorType, - actorId: actor.actorId, - agentId: actor.agentId, - runId: actor.runId, - agentApiKeyId: actor.agentApiKeyId, - action: "agent.config_rolled_back", - entityType: "agent", - entityId: updated.id, - details: { revisionId }, - }); + await logActivity(db, { + companyId: updated.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, + action: "agent.config_rolled_back", + entityType: "agent", + entityId: updated.id, + details: { revisionId }, + }); - res.json(updated); - }); + res.json(updated); + }, + ); router.get("/agents/:id/runtime-state", async (req, res) => { assertBoard(req); const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!agent) return; await assertBoardCanManageAgentsForCompany(req, agent.companyId); @@ -3095,7 +3678,12 @@ export function agentRoutes( router.get("/agents/:id/task-sessions", async (req, res) => { assertBoard(req); const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!agent) return; await assertBoardCanManageAgentsForCompany(req, agent.companyId); @@ -3103,223 +3691,241 @@ export function agentRoutes( res.json( sessions.map((session) => ({ ...session, - sessionParamsJson: redactEventPayload(session.sessionParamsJson ?? null), + sessionParamsJson: redactEventPayload( + session.sessionParamsJson ?? null, + ), })), ); }); - router.post("/agents/:id/runtime-state/reset-session", validate(resetAgentSessionSchema), async (req, res) => { - assertBoard(req); - const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); - if (!agent) return; - await assertBoardCanManageAgentsForCompany(req, agent.companyId); + router.post( + "/agents/:id/runtime-state/reset-session", + validate(resetAgentSessionSchema), + async (req, res) => { + assertBoard(req); + const id = req.params.id as string; + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); + if (!agent) return; + await assertBoardCanManageAgentsForCompany(req, agent.companyId); - const taskKey = - typeof req.body.taskKey === "string" && req.body.taskKey.trim().length > 0 - ? req.body.taskKey.trim() - : null; - const state = await heartbeat.resetRuntimeSession(id, { taskKey }); + const taskKey = + typeof req.body.taskKey === "string" && + req.body.taskKey.trim().length > 0 + ? req.body.taskKey.trim() + : null; + const state = await heartbeat.resetRuntimeSession(id, { taskKey }); - await logActivity(db, { - companyId: agent.companyId, - actorType: "user", - actorId: req.actor.userId ?? "board", - action: "agent.runtime_session_reset", - entityType: "agent", - entityId: id, - details: { taskKey: taskKey ?? null }, - }); + await logActivity(db, { + companyId: agent.companyId, + actorType: "user", + actorId: req.actor.userId ?? "board", + action: "agent.runtime_session_reset", + entityType: "agent", + entityId: id, + details: { taskKey: taskKey ?? null }, + }); - res.json(state); - }); + res.json(state); + }, + ); - router.post("/companies/:companyId/agent-hires", validate(createAgentHireSchema), async (req, res) => { - const companyId = req.params.companyId as string; - await assertCanCreateAgentsForCompany(req, companyId); - const sourceIssueIds = parseSourceIssueIds(req.body); - const { - desiredSkills: requestedDesiredSkills, - instructionsBundle, - sourceIssueId: _sourceIssueId, - sourceIssueIds: _sourceIssueIds, - // The stored-session claim is not an agent column. The server derives the - // owner from the authenticated actor and consumes the claim in the create - // transaction, so it never reaches the insert values. - storedSessionId: hireStoredSessionId, - // The apply-existing flag is not an agent column. The server binds the - // fixed reference to the owner stored value with no login round trip. - applyStoredClaudeLogin: hireApplyStoredClaudeLogin, - ...hireInput - } = req.body; - hireInput.adapterType = assertSelectableAdapterType(hireInput.adapterType); - const rawHireAdapterConfig = (hireInput.adapterConfig ?? {}) as Record; - assertNoNewAgentLegacyPromptTemplate( - hireInput.adapterType, - rawHireAdapterConfig, - ); - assertNoAgentAdapterConfigMutation(req, rawHireAdapterConfig); - assertNoAgentRuntimeConfigAdapterConfigMutation(req, hireInput.runtimeConfig); - const hiredAgentId = randomUUID(); - const requestedAdapterConfig = applyCodexLocalKeyIsolation( - companyId, - hiredAgentId, - hireInput.adapterType, - applyCreateDefaultsByAdapterType( + router.post( + "/companies/:companyId/agent-hires", + validate(createAgentHireSchema), + async (req, res) => { + const companyId = req.params.companyId as string; + await assertCanCreateAgentsForCompany(req, companyId); + const sourceIssueIds = parseSourceIssueIds(req.body); + const { + desiredSkills: requestedDesiredSkills, + instructionsBundle, + sourceIssueId: _sourceIssueId, + sourceIssueIds: _sourceIssueIds, + // The stored-session claim is not an agent column. The server derives the + // owner from the authenticated actor and consumes the claim in the create + // transaction, so it never reaches the insert values. + storedSessionId: hireStoredSessionId, + // The apply-existing flag is not an agent column. The server binds the + // fixed reference to the owner stored value with no login round trip. + applyStoredClaudeLogin: hireApplyStoredClaudeLogin, + ...hireInput + } = req.body; + hireInput.adapterType = assertSelectableAdapterType( + hireInput.adapterType, + ); + const rawHireAdapterConfig = (hireInput.adapterConfig ?? {}) as Record< + string, + unknown + >; + assertNoNewAgentLegacyPromptTemplate( hireInput.adapterType, rawHireAdapterConfig, - ), - ); - const desiredSkillAssignment = await resolveDesiredSkillAssignment( - companyId, - hireInput.adapterType, - requestedAdapterConfig, - normalizeDesiredSkillSelections(Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined), - "add", - ); - const normalizedAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({ - companyId, - adapterType: hireInput.adapterType, - adapterConfig: desiredSkillAssignment.adapterConfig, - }); - const normalizedRuntimeConfig = await normalizeRuntimeConfigAdapterConfigsForPersistence( - companyId, - hireInput.adapterType, - await normalizeNewAgentRuntimeConfig(hireInput.adapterType, hireInput.runtimeConfig), - normalizedAdapterConfig, - ); - const normalizedHireInput = { - ...hireInput, - adapterConfig: normalizedAdapterConfig, - runtimeConfig: normalizedRuntimeConfig, - }; + ); + assertNoAgentAdapterConfigMutation(req, rawHireAdapterConfig); + assertNoAgentRuntimeConfigAdapterConfigMutation( + req, + hireInput.runtimeConfig, + ); + const hiredAgentId = randomUUID(); + const requestedAdapterConfig = applyCodexLocalKeyIsolation( + companyId, + hiredAgentId, + hireInput.adapterType, + applyCreateDefaultsByAdapterType( + hireInput.adapterType, + rawHireAdapterConfig, + ), + ); + const desiredSkillAssignment = await resolveDesiredSkillAssignment( + companyId, + hireInput.adapterType, + requestedAdapterConfig, + normalizeDesiredSkillSelections( + Array.isArray(requestedDesiredSkills) + ? requestedDesiredSkills + : undefined, + ), + "add", + ); + const normalizedAdapterConfig = + await normalizeMediatedAdapterConfigForPersistence({ + companyId, + adapterType: hireInput.adapterType, + adapterConfig: desiredSkillAssignment.adapterConfig, + }); + const normalizedRuntimeConfig = + await normalizeRuntimeConfigAdapterConfigsForPersistence( + companyId, + hireInput.adapterType, + await normalizeNewAgentRuntimeConfig( + hireInput.adapterType, + hireInput.runtimeConfig, + ), + normalizedAdapterConfig, + ); + const normalizedHireInput = { + ...hireInput, + adapterConfig: normalizedAdapterConfig, + runtimeConfig: normalizedRuntimeConfig, + }; - const company = await db - .select() - .from(companies) - .where(eq(companies.id, companyId)) - .then((rows) => rows[0] ?? null); - if (!company) { - res.status(404).json({ error: "Company not found" }); - return; - } + const company = await db + .select() + .from(companies) + .where(eq(companies.id, companyId)) + .then((rows) => rows[0] ?? null); + if (!company) { + res.status(404).json({ error: "Company not found" }); + return; + } - const requiresApproval = company.requireBoardApprovalForNewAgents; - const status = requiresApproval ? "pending_approval" : "idle"; - const createdAgent = await svc.create( - companyId, - { - id: hiredAgentId, - ...normalizedHireInput, - status, - spentMonthlyCents: 0, - lastHeartbeatAt: null, - }, - { - claudeLogin: { - storedSessionId: hireStoredSessionId ?? null, - ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null), - // The apply-existing path runs only for a user actor. The owner comes - // from the actor, so an agent actor never reaches the no-claim bind. - applyExistingWithoutClaim: - req.actor.type !== "agent" && hireApplyStoredClaudeLogin === true, + const requiresApproval = company.requireBoardApprovalForNewAgents; + const status = requiresApproval ? "pending_approval" : "idle"; + const createdAgent = await svc.create( + companyId, + { + id: hiredAgentId, + ...normalizedHireInput, + status, + spentMonthlyCents: 0, + lastHeartbeatAt: null, }, - }, - ); - const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent, instructionsBundle); + { + claudeLogin: { + storedSessionId: hireStoredSessionId ?? null, + ownerUserId: + req.actor.type === "agent" ? null : (req.actor.userId ?? null), + // The apply-existing path runs only for a user actor. The owner comes + // from the actor, so an agent actor never reaches the no-claim bind. + applyExistingWithoutClaim: + req.actor.type !== "agent" && hireApplyStoredClaudeLogin === true, + }, + }, + ); + const agent = await materializeDefaultInstructionsBundleForNewAgent( + createdAgent, + instructionsBundle, + ); - let approval: Awaited> | null = null; - const actor = getActorInfo(req); + let approval: Awaited> | null = + null; + const actor = getActorInfo(req); - if (requiresApproval) { - const requestedAdapterType = normalizedHireInput.adapterType ?? agent.adapterType; - const requestedAdapterConfig = - redactEventPayload( - (agent.adapterConfig ?? normalizedHireInput.adapterConfig) as Record, - ) ?? {}; - const requestedRuntimeConfig = - redactEventPayload( - (normalizedHireInput.runtimeConfig ?? agent.runtimeConfig) as Record, - ) ?? {}; - const requestedMetadata = - redactEventPayload( - ((normalizedHireInput.metadata ?? agent.metadata ?? {}) as Record), - ) ?? {}; - approval = await approvalsSvc.create(companyId, { - type: "hire_agent", - requestedByAgentId: actor.actorType === "agent" ? actor.actorId : null, - requestedByUserId: actor.actorType === "user" ? actor.actorId : null, - status: "pending", - payload: { - name: normalizedHireInput.name, - role: normalizedHireInput.role, - title: normalizedHireInput.title ?? null, - icon: normalizedHireInput.icon ?? null, - reportsTo: normalizedHireInput.reportsTo ?? null, - capabilities: normalizedHireInput.capabilities ?? null, - adapterType: requestedAdapterType, - adapterConfig: requestedAdapterConfig, - runtimeConfig: requestedRuntimeConfig, - budgetMonthlyCents: - typeof normalizedHireInput.budgetMonthlyCents === "number" - ? normalizedHireInput.budgetMonthlyCents - : agent.budgetMonthlyCents, - desiredSkills: desiredSkillAssignment.desiredSkills, - metadata: requestedMetadata, - agentId: agent.id, - requestedByAgentId: actor.actorType === "agent" ? actor.actorId : null, - requestedConfigurationSnapshot: { + if (requiresApproval) { + const requestedAdapterType = + normalizedHireInput.adapterType ?? agent.adapterType; + const requestedAdapterConfig = + redactEventPayload( + (agent.adapterConfig ?? + normalizedHireInput.adapterConfig) as Record, + ) ?? {}; + const requestedRuntimeConfig = + redactEventPayload( + (normalizedHireInput.runtimeConfig ?? + agent.runtimeConfig) as Record, + ) ?? {}; + const requestedMetadata = + redactEventPayload( + (normalizedHireInput.metadata ?? agent.metadata ?? {}) as Record< + string, + unknown + >, + ) ?? {}; + approval = await approvalsSvc.create(companyId, { + type: "hire_agent", + requestedByAgentId: + actor.actorType === "agent" ? actor.actorId : null, + requestedByUserId: actor.actorType === "user" ? actor.actorId : null, + status: "pending", + payload: { + name: normalizedHireInput.name, + role: normalizedHireInput.role, + title: normalizedHireInput.title ?? null, + icon: normalizedHireInput.icon ?? null, + reportsTo: normalizedHireInput.reportsTo ?? null, + capabilities: normalizedHireInput.capabilities ?? null, adapterType: requestedAdapterType, adapterConfig: requestedAdapterConfig, runtimeConfig: requestedRuntimeConfig, + budgetMonthlyCents: + typeof normalizedHireInput.budgetMonthlyCents === "number" + ? normalizedHireInput.budgetMonthlyCents + : agent.budgetMonthlyCents, desiredSkills: desiredSkillAssignment.desiredSkills, + metadata: requestedMetadata, + agentId: agent.id, + requestedByAgentId: + actor.actorType === "agent" ? actor.actorId : null, + requestedConfigurationSnapshot: { + adapterType: requestedAdapterType, + adapterConfig: requestedAdapterConfig, + runtimeConfig: requestedRuntimeConfig, + desiredSkills: desiredSkillAssignment.desiredSkills, + }, }, - }, - decisionNote: null, - decidedByUserId: null, - decidedAt: null, - updatedAt: new Date(), - }); - - if (sourceIssueIds.length > 0) { - await issueApprovalsSvc.linkManyForApproval(approval.id, sourceIssueIds, { - agentId: actor.actorType === "agent" ? actor.actorId : null, - userId: actor.actorType === "user" ? actor.actorId : null, + decisionNote: null, + decidedByUserId: null, + decidedAt: null, + updatedAt: new Date(), }); + + if (sourceIssueIds.length > 0) { + await issueApprovalsSvc.linkManyForApproval( + approval.id, + sourceIssueIds, + { + agentId: actor.actorType === "agent" ? actor.actorId : null, + userId: actor.actorType === "user" ? actor.actorId : null, + }, + ); + } } - } - await logActivity(db, { - companyId, - actorType: actor.actorType, - actorId: actor.actorId, - agentId: actor.agentId, - runId: actor.runId, - agentApiKeyId: actor.agentApiKeyId, - action: "agent.hire_created", - entityType: "agent", - entityId: agent.id, - details: { - name: agent.name, - role: agent.role, - requiresApproval, - approvalId: approval?.id ?? null, - issueIds: sourceIssueIds, - desiredSkills: desiredSkillAssignment.desiredSkills, - }, - }); - const telemetryClient = getTelemetryClient(); - if (telemetryClient) { - trackAgentCreated(telemetryClient, { agentRole: agent.role, agentId: agent.id }); - } - - await applyDefaultAgentTaskAssignGrant( - companyId, - agent.id, - actor.actorType === "user" ? actor.actorId : null, - ); - - if (approval) { await logActivity(db, { companyId, actorType: actor.actorType, @@ -3327,358 +3933,498 @@ export function agentRoutes( agentId: actor.agentId, runId: actor.runId, agentApiKeyId: actor.agentApiKeyId, - action: "approval.created", - entityType: "approval", - entityId: approval.id, - details: { type: approval.type, linkedAgentId: agent.id }, + action: "agent.hire_created", + entityType: "agent", + entityId: agent.id, + details: { + name: agent.name, + role: agent.role, + requiresApproval, + approvalId: approval?.id ?? null, + issueIds: sourceIssueIds, + desiredSkills: desiredSkillAssignment.desiredSkills, + }, }); - } + const telemetryClient = getTelemetryClient(); + if (telemetryClient) { + trackAgentCreated(telemetryClient, { + agentRole: agent.role, + agentId: agent.id, + }); + } - res.status(201).json({ agent, approval }); - }); - - router.post("/companies/:companyId/agents", validate(createAgentSchema), async (req, res) => { - const companyId = req.params.companyId as string; - await assertCanCreateAgentsForCompany(req, companyId); - - const company = await db - .select() - .from(companies) - .where(eq(companies.id, companyId)) - .then((rows) => rows[0] ?? null); - if (!company) { - res.status(404).json({ error: "Company not found" }); - return; - } - if (company.requireBoardApprovalForNewAgents) { - throw conflict( - "Direct agent creation requires board approval. Use POST /api/companies/:companyId/agent-hires to create a pending hire approval.", - ); - } - - const { - desiredSkills: requestedDesiredSkills, - instructionsBundle, - // The stored-session claim is not an agent column. The server derives the - // owner from the authenticated actor and consumes the claim in the create - // transaction, so it never reaches the insert values. - storedSessionId: createStoredSessionId, - // The apply-existing flag is not an agent column. The server binds the - // fixed reference to the owner stored value with no login round trip. - applyStoredClaudeLogin: createApplyStoredClaudeLogin, - ...createInput - } = req.body; - createInput.adapterType = assertSelectableAdapterType(createInput.adapterType); - const rawCreateAdapterConfig = (createInput.adapterConfig ?? {}) as Record; - assertNoNewAgentLegacyPromptTemplate( - createInput.adapterType, - rawCreateAdapterConfig, - ); - assertNoAgentAdapterConfigMutation(req, rawCreateAdapterConfig); - assertNoAgentRuntimeConfigAdapterConfigMutation(req, createInput.runtimeConfig); - const agentId = randomUUID(); - const requestedAdapterConfig = applyCodexLocalKeyIsolation( - companyId, - agentId, - createInput.adapterType, - applyCreateDefaultsByAdapterType( - createInput.adapterType, - rawCreateAdapterConfig, - ), - ); - const desiredSkillAssignment = await resolveDesiredSkillAssignment( - companyId, - createInput.adapterType, - requestedAdapterConfig, - normalizeDesiredSkillSelections(Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined), - "add", - ); - const normalizedAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({ - companyId, - adapterType: createInput.adapterType, - adapterConfig: desiredSkillAssignment.adapterConfig, - }); - const normalizedRuntimeConfig = await normalizeRuntimeConfigAdapterConfigsForPersistence( - companyId, - createInput.adapterType, - await normalizeNewAgentRuntimeConfig(createInput.adapterType, createInput.runtimeConfig), - normalizedAdapterConfig, - ); - await assertAgentEnvironmentSelection(companyId, createInput.adapterType, createInput.defaultEnvironmentId); - await assertAgentDefaultEnvironmentSelection(companyId, createInput.defaultEnvironmentId, { - allowedDrivers: allowedEnvironmentDriversForAgent(createInput.adapterType), - allowedSandboxProviders: allowedSandboxProvidersForAgent(createInput.adapterType), - }); - - const createdAgent = await svc.create( - companyId, - { - id: agentId, - ...createInput, - adapterConfig: normalizedAdapterConfig, - runtimeConfig: normalizedRuntimeConfig, - status: "idle", - spentMonthlyCents: 0, - lastHeartbeatAt: null, - }, - { - claudeLogin: { - storedSessionId: createStoredSessionId ?? null, - ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null), - // The apply-existing path runs only for a user actor. The owner comes - // from the actor, so an agent actor never reaches the no-claim bind. - applyExistingWithoutClaim: - req.actor.type !== "agent" && createApplyStoredClaudeLogin === true, - }, - }, - ); - const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent, instructionsBundle); - - const actor = getActorInfo(req); - await logActivity(db, { - companyId, - actorType: actor.actorType, - actorId: actor.actorId, - agentId: actor.agentId, - runId: actor.runId, - agentApiKeyId: actor.agentApiKeyId, - action: "agent.created", - entityType: "agent", - entityId: agent.id, - details: { - name: agent.name, - role: agent.role, - desiredSkills: desiredSkillAssignment.desiredSkills, - }, - }); - const telemetryClient = getTelemetryClient(); - if (telemetryClient) { - trackAgentCreated(telemetryClient, { agentRole: agent.role, agentId: agent.id }); - } - - await applyDefaultAgentTaskAssignGrant( - companyId, - agent.id, - req.actor.type === "board" ? (req.actor.userId ?? null) : null, - ); - await builtInAgentService(db).ensureCompanyDefaultAgentGrants(companyId); - - if (agent.budgetMonthlyCents > 0) { - await budgets.upsertPolicy( + await applyDefaultAgentTaskAssignGrant( companyId, - { - scopeType: "agent", - scopeId: agent.id, - amount: agent.budgetMonthlyCents, - windowKind: "calendar_month_utc", - }, + agent.id, actor.actorType === "user" ? actor.actorId : null, ); - } - res.status(201).json(agent); - }); + if (approval) { + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, + action: "approval.created", + entityType: "approval", + entityId: approval.id, + details: { type: approval.type, linkedAgentId: agent.id }, + }); + } - router.patch("/agents/:id/permissions", validate(updateAgentPermissionsSchema), async (req, res) => { - const id = req.params.id as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); - if (!existing) return; + res.status(201).json({ agent, approval }); + }, + ); - if (req.actor.type === "agent") { - const actorAgent = req.actor.agentId ? await svc.getById(req.actor.agentId) : null; - if (!actorAgent || actorAgent.companyId !== existing.companyId) { - res.status(403).json({ error: "Forbidden" }); + router.post( + "/companies/:companyId/agents", + validate(createAgentSchema), + async (req, res) => { + const companyId = req.params.companyId as string; + await assertCanCreateAgentsForCompany(req, companyId); + + const company = await db + .select() + .from(companies) + .where(eq(companies.id, companyId)) + .then((rows) => rows[0] ?? null); + if (!company) { + res.status(404).json({ error: "Company not found" }); return; } - if (actorAgent.role !== "ceo") { - res.status(403).json({ error: "Only CEO can manage permissions" }); - return; + if (company.requireBoardApprovalForNewAgents) { + throw conflict( + "Direct agent creation requires board approval. Use POST /api/companies/:companyId/agent-hires to create a pending hire approval.", + ); } - } else { - await assertBoardCanManageAgentsForCompany(req, existing.companyId); - } - const agent = await svc.updatePermissions(id, req.body); - if (!agent) { - res.status(404).json({ error: "Agent not found" }); - return; - } - - const effectiveCanAssignTasks = - agent.role === "ceo" || Boolean(agent.permissions?.canCreateAgents) || req.body.canAssignTasks; - await access.ensureMembership(agent.companyId, "agent", agent.id, "member", "active"); - await access.setPrincipalPermission( - agent.companyId, - "agent", - agent.id, - "tasks:assign", - effectiveCanAssignTasks, - req.actor.type === "board" ? (req.actor.userId ?? null) : null, - ); - - const actor = getActorInfo(req); - await logActivity(db, { - companyId: agent.companyId, - actorType: actor.actorType, - actorId: actor.actorId, - agentId: actor.agentId, - runId: actor.runId, - agentApiKeyId: actor.agentApiKeyId, - action: "agent.permissions_updated", - entityType: "agent", - entityId: agent.id, - details: { - canCreateAgents: agent.permissions?.canCreateAgents ?? false, - canCreateSkills: agent.permissions?.canCreateSkills ?? true, - canAssignTasks: effectiveCanAssignTasks, - trustPreset: agent.permissions?.trustPreset ?? "standard", - }, - }); - - res.json(await buildAgentDetail(agent)); - }); - - router.patch("/agents/:id/instructions-path", validate(updateAgentInstructionsPathSchema), async (req, res) => { - if (req.actor.type !== "board") { - throw forbidden("Only board-authenticated callers can manage instructions path or bundle configuration"); - } - - const id = req.params.id as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); - if (!existing) return; - - await assertCanManageInstructionsPath(req, existing); - - const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {}; - const explicitKey = asNonEmptyString(req.body.adapterConfigKey); - const defaultKey = resolveInstructionsPathKey(existing.adapterType); - const adapterConfigKey = explicitKey ?? defaultKey; - if (!adapterConfigKey) { - res.status(422).json({ - error: `No default instructions path key for adapter type '${existing.adapterType}'. Provide adapterConfigKey.`, - }); - return; - } - - const nextAdapterConfig: Record = { ...existingAdapterConfig }; - if (req.body.path === null) { - delete nextAdapterConfig[adapterConfigKey]; - } else { - nextAdapterConfig[adapterConfigKey] = resolveInstructionsFilePath(req.body.path, existingAdapterConfig); - } - - const syncedAdapterConfig = syncInstructionsBundleConfigFromFilePath(existing, nextAdapterConfig); - const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence( - existing.companyId, - syncedAdapterConfig, - { strictMode: strictSecretsMode, adapterType: existing.adapterType }, - ); - const actor = getActorInfo(req); - const agent = await svc.update( - id, - { adapterConfig: normalizedAdapterConfig }, - { - recordRevision: { - createdByAgentId: actor.agentId, - createdByUserId: actor.actorType === "user" ? actor.actorId : null, - source: "instructions_path_patch", + const { + desiredSkills: requestedDesiredSkills, + instructionsBundle, + // The stored-session claim is not an agent column. The server derives the + // owner from the authenticated actor and consumes the claim in the create + // transaction, so it never reaches the insert values. + storedSessionId: createStoredSessionId, + // The apply-existing flag is not an agent column. The server binds the + // fixed reference to the owner stored value with no login round trip. + applyStoredClaudeLogin: createApplyStoredClaudeLogin, + ...createInput + } = req.body; + createInput.adapterType = assertSelectableAdapterType( + createInput.adapterType, + ); + const rawCreateAdapterConfig = (createInput.adapterConfig ?? + {}) as Record; + assertNoNewAgentLegacyPromptTemplate( + createInput.adapterType, + rawCreateAdapterConfig, + ); + assertNoAgentAdapterConfigMutation(req, rawCreateAdapterConfig); + assertNoAgentRuntimeConfigAdapterConfigMutation( + req, + createInput.runtimeConfig, + ); + const agentId = randomUUID(); + const requestedAdapterConfig = applyCodexLocalKeyIsolation( + companyId, + agentId, + createInput.adapterType, + applyCreateDefaultsByAdapterType( + createInput.adapterType, + rawCreateAdapterConfig, + ), + ); + const desiredSkillAssignment = await resolveDesiredSkillAssignment( + companyId, + createInput.adapterType, + requestedAdapterConfig, + normalizeDesiredSkillSelections( + Array.isArray(requestedDesiredSkills) + ? requestedDesiredSkills + : undefined, + ), + "add", + ); + const normalizedAdapterConfig = + await normalizeMediatedAdapterConfigForPersistence({ + companyId, + adapterType: createInput.adapterType, + adapterConfig: desiredSkillAssignment.adapterConfig, + }); + const normalizedRuntimeConfig = + await normalizeRuntimeConfigAdapterConfigsForPersistence( + companyId, + createInput.adapterType, + await normalizeNewAgentRuntimeConfig( + createInput.adapterType, + createInput.runtimeConfig, + ), + normalizedAdapterConfig, + ); + await assertAgentEnvironmentSelection( + companyId, + createInput.adapterType, + createInput.defaultEnvironmentId, + ); + await assertAgentDefaultEnvironmentSelection( + companyId, + createInput.defaultEnvironmentId, + { + allowedDrivers: allowedEnvironmentDriversForAgent( + createInput.adapterType, + ), + allowedSandboxProviders: allowedSandboxProvidersForAgent( + createInput.adapterType, + ), }, - }, - ); - if (!agent) { - res.status(404).json({ error: "Agent not found" }); - return; - } + ); - const updatedAdapterConfig = asRecord(agent.adapterConfig) ?? {}; - const pathValue = asNonEmptyString(updatedAdapterConfig[adapterConfigKey]); + const createdAgent = await svc.create( + companyId, + { + id: agentId, + ...createInput, + adapterConfig: normalizedAdapterConfig, + runtimeConfig: normalizedRuntimeConfig, + status: "idle", + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }, + { + claudeLogin: { + storedSessionId: createStoredSessionId ?? null, + ownerUserId: + req.actor.type === "agent" ? null : (req.actor.userId ?? null), + // The apply-existing path runs only for a user actor. The owner comes + // from the actor, so an agent actor never reaches the no-claim bind. + applyExistingWithoutClaim: + req.actor.type !== "agent" && + createApplyStoredClaudeLogin === true, + }, + }, + ); + const agent = await materializeDefaultInstructionsBundleForNewAgent( + createdAgent, + instructionsBundle, + ); - await logActivity(db, { - companyId: agent.companyId, - actorType: actor.actorType, - actorId: actor.actorId, - agentId: actor.agentId, - runId: actor.runId, - agentApiKeyId: actor.agentApiKeyId, - action: "agent.instructions_path_updated", - entityType: "agent", - entityId: agent.id, - details: { + const actor = getActorInfo(req); + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, + action: "agent.created", + entityType: "agent", + entityId: agent.id, + details: { + name: agent.name, + role: agent.role, + desiredSkills: desiredSkillAssignment.desiredSkills, + }, + }); + const telemetryClient = getTelemetryClient(); + if (telemetryClient) { + trackAgentCreated(telemetryClient, { + agentRole: agent.role, + agentId: agent.id, + }); + } + + await applyDefaultAgentTaskAssignGrant( + companyId, + agent.id, + req.actor.type === "board" ? (req.actor.userId ?? null) : null, + ); + await builtInAgentService(db).ensureCompanyDefaultAgentGrants(companyId); + + if (agent.budgetMonthlyCents > 0) { + await budgets.upsertPolicy( + companyId, + { + scopeType: "agent", + scopeId: agent.id, + amount: agent.budgetMonthlyCents, + windowKind: "calendar_month_utc", + }, + actor.actorType === "user" ? actor.actorId : null, + ); + } + + res.status(201).json(agent); + }, + ); + + router.patch( + "/agents/:id/permissions", + validate(updateAgentPermissionsSchema), + async (req, res) => { + const id = req.params.id as string; + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); + if (!existing) return; + + if (req.actor.type === "agent") { + const actorAgent = req.actor.agentId + ? await svc.getById(req.actor.agentId) + : null; + if (!actorAgent || actorAgent.companyId !== existing.companyId) { + res.status(403).json({ error: "Forbidden" }); + return; + } + if (actorAgent.role !== "ceo") { + res.status(403).json({ error: "Only CEO can manage permissions" }); + return; + } + } else { + await assertBoardCanManageAgentsForCompany(req, existing.companyId); + } + + const agent = await svc.updatePermissions(id, req.body); + if (!agent) { + res.status(404).json({ error: "Agent not found" }); + return; + } + + const effectiveCanAssignTasks = + agent.role === "ceo" || + Boolean(agent.permissions?.canCreateAgents) || + req.body.canAssignTasks; + await access.ensureMembership( + agent.companyId, + "agent", + agent.id, + "member", + "active", + ); + await access.setPrincipalPermission( + agent.companyId, + "agent", + agent.id, + "tasks:assign", + effectiveCanAssignTasks, + req.actor.type === "board" ? (req.actor.userId ?? null) : null, + ); + + const actor = getActorInfo(req); + await logActivity(db, { + companyId: agent.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, + action: "agent.permissions_updated", + entityType: "agent", + entityId: agent.id, + details: { + canCreateAgents: agent.permissions?.canCreateAgents ?? false, + canCreateSkills: agent.permissions?.canCreateSkills ?? true, + canAssignTasks: effectiveCanAssignTasks, + trustPreset: agent.permissions?.trustPreset ?? "standard", + }, + }); + + res.json(await buildAgentDetail(agent)); + }, + ); + + router.patch( + "/agents/:id/instructions-path", + validate(updateAgentInstructionsPathSchema), + async (req, res) => { + if (req.actor.type !== "board") { + throw forbidden( + "Only board-authenticated callers can manage instructions path or bundle configuration", + ); + } + + const id = req.params.id as string; + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); + if (!existing) return; + + await assertCanManageInstructionsPath(req, existing); + + const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {}; + const explicitKey = asNonEmptyString(req.body.adapterConfigKey); + const defaultKey = resolveInstructionsPathKey(existing.adapterType); + const adapterConfigKey = explicitKey ?? defaultKey; + if (!adapterConfigKey) { + res.status(422).json({ + error: `No default instructions path key for adapter type '${existing.adapterType}'. Provide adapterConfigKey.`, + }); + return; + } + + const nextAdapterConfig: Record = { + ...existingAdapterConfig, + }; + if (req.body.path === null) { + delete nextAdapterConfig[adapterConfigKey]; + } else { + nextAdapterConfig[adapterConfigKey] = resolveInstructionsFilePath( + req.body.path, + existingAdapterConfig, + ); + } + + const syncedAdapterConfig = syncInstructionsBundleConfigFromFilePath( + existing, + nextAdapterConfig, + ); + const normalizedAdapterConfig = + await secretsSvc.normalizeAdapterConfigForPersistence( + existing.companyId, + syncedAdapterConfig, + { strictMode: strictSecretsMode, adapterType: existing.adapterType }, + ); + const actor = getActorInfo(req); + const agent = await svc.update( + id, + { adapterConfig: normalizedAdapterConfig }, + { + recordRevision: { + createdByAgentId: actor.agentId, + createdByUserId: actor.actorType === "user" ? actor.actorId : null, + source: "instructions_path_patch", + }, + }, + ); + if (!agent) { + res.status(404).json({ error: "Agent not found" }); + return; + } + + const updatedAdapterConfig = asRecord(agent.adapterConfig) ?? {}; + const pathValue = asNonEmptyString( + updatedAdapterConfig[adapterConfigKey], + ); + + await logActivity(db, { + companyId: agent.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, + action: "agent.instructions_path_updated", + entityType: "agent", + entityId: agent.id, + details: { + adapterConfigKey, + path: pathValue, + cleared: req.body.path === null, + }, + }); + + res.json({ + agentId: agent.id, + adapterType: agent.adapterType, adapterConfigKey, path: pathValue, - cleared: req.body.path === null, - }, - }); - - res.json({ - agentId: agent.id, - adapterType: agent.adapterType, - adapterConfigKey, - path: pathValue, - }); - }); + }); + }, + ); router.get("/agents/:id/instructions-bundle", async (req, res) => { const id = req.params.id as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!existing) return; await assertCanReadAgent(req, existing); res.json(await instructions.getBundle(existing)); }); - router.patch("/agents/:id/instructions-bundle", validate(updateAgentInstructionsBundleSchema), async (req, res) => { - const id = req.params.id as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); - if (!existing) return; - await assertCanManageInstructionsPath(req, existing); + router.patch( + "/agents/:id/instructions-bundle", + validate(updateAgentInstructionsBundleSchema), + async (req, res) => { + const id = req.params.id as string; + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); + if (!existing) return; + await assertCanManageInstructionsPath(req, existing); - const actor = getActorInfo(req); - const { bundle, adapterConfig } = await instructions.updateBundle(existing, req.body); - const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence( - existing.companyId, - adapterConfig, - { strictMode: strictSecretsMode, adapterType: existing.adapterType }, - ); - await svc.update( - id, - { adapterConfig: normalizedAdapterConfig }, - { - recordRevision: { - createdByAgentId: actor.agentId, - createdByUserId: actor.actorType === "user" ? actor.actorId : null, - source: "instructions_bundle_patch", + const actor = getActorInfo(req); + const { bundle, adapterConfig } = await instructions.updateBundle( + existing, + req.body, + ); + const normalizedAdapterConfig = + await secretsSvc.normalizeAdapterConfigForPersistence( + existing.companyId, + adapterConfig, + { strictMode: strictSecretsMode, adapterType: existing.adapterType }, + ); + await svc.update( + id, + { adapterConfig: normalizedAdapterConfig }, + { + recordRevision: { + createdByAgentId: actor.agentId, + createdByUserId: actor.actorType === "user" ? actor.actorId : null, + source: "instructions_bundle_patch", + }, }, - }, - ); + ); - await logActivity(db, { - companyId: existing.companyId, - actorType: actor.actorType, - actorId: actor.actorId, - agentId: actor.agentId, - runId: actor.runId, - agentApiKeyId: actor.agentApiKeyId, - action: "agent.instructions_bundle_updated", - entityType: "agent", - entityId: existing.id, - details: { - mode: bundle.mode, - rootPath: bundle.rootPath, - entryFile: bundle.entryFile, - clearLegacyPromptTemplate: req.body.clearLegacyPromptTemplate === true, - }, - }); + await logActivity(db, { + companyId: existing.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, + action: "agent.instructions_bundle_updated", + entityType: "agent", + entityId: existing.id, + details: { + mode: bundle.mode, + rootPath: bundle.rootPath, + entryFile: bundle.entryFile, + clearLegacyPromptTemplate: + req.body.clearLegacyPromptTemplate === true, + }, + }); - res.json(bundle); - }); + res.json(bundle); + }, + ); router.get("/agents/:id/instructions-bundle/file", async (req, res) => { const id = req.params.id as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!existing) return; await assertCanReadAgent(req, existing); - const relativePath = typeof req.query.path === "string" ? req.query.path : ""; + const relativePath = + typeof req.query.path === "string" ? req.query.path : ""; if (!relativePath.trim()) { res.status(422).json({ error: "Query parameter 'path' is required" }); return; @@ -3687,60 +4433,82 @@ export function agentRoutes( res.json(await instructions.readFile(existing, relativePath)); }); - router.put("/agents/:id/instructions-bundle/file", validate(upsertAgentInstructionsFileSchema), async (req, res) => { - const id = req.params.id as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); - if (!existing) return; - await assertCanManageInstructionsPath(req, existing); + router.put( + "/agents/:id/instructions-bundle/file", + validate(upsertAgentInstructionsFileSchema), + async (req, res) => { + const id = req.params.id as string; + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); + if (!existing) return; + await assertCanManageInstructionsPath(req, existing); - const actor = getActorInfo(req); - const result = await instructions.writeFile(existing, req.body.path, req.body.content, { - clearLegacyPromptTemplate: req.body.clearLegacyPromptTemplate, - }); - const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence( - existing.companyId, - result.adapterConfig, - { strictMode: strictSecretsMode, adapterType: existing.adapterType }, - ); - await svc.update( - id, - { adapterConfig: normalizedAdapterConfig }, - { - recordRevision: { - createdByAgentId: actor.agentId, - createdByUserId: actor.actorType === "user" ? actor.actorId : null, - source: "instructions_bundle_file_put", + const actor = getActorInfo(req); + const result = await instructions.writeFile( + existing, + req.body.path, + req.body.content, + { + clearLegacyPromptTemplate: req.body.clearLegacyPromptTemplate, }, - }, - ); + ); + const normalizedAdapterConfig = + await secretsSvc.normalizeAdapterConfigForPersistence( + existing.companyId, + result.adapterConfig, + { strictMode: strictSecretsMode, adapterType: existing.adapterType }, + ); + await svc.update( + id, + { adapterConfig: normalizedAdapterConfig }, + { + recordRevision: { + createdByAgentId: actor.agentId, + createdByUserId: actor.actorType === "user" ? actor.actorId : null, + source: "instructions_bundle_file_put", + }, + }, + ); - await logActivity(db, { - companyId: existing.companyId, - actorType: actor.actorType, - actorId: actor.actorId, - agentId: actor.agentId, - runId: actor.runId, - agentApiKeyId: actor.agentApiKeyId, - action: "agent.instructions_file_updated", - entityType: "agent", - entityId: existing.id, - details: { - path: result.file.path, - size: result.file.size, - clearLegacyPromptTemplate: req.body.clearLegacyPromptTemplate === true, - }, - }); + await logActivity(db, { + companyId: existing.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, + action: "agent.instructions_file_updated", + entityType: "agent", + entityId: existing.id, + details: { + path: result.file.path, + size: result.file.size, + clearLegacyPromptTemplate: + req.body.clearLegacyPromptTemplate === true, + }, + }); - res.json(result.file); - }); + res.json(result.file); + }, + ); router.delete("/agents/:id/instructions-bundle/file", async (req, res) => { const id = req.params.id as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!existing) return; await assertCanManageInstructionsPath(req, existing); - const relativePath = typeof req.query.path === "string" ? req.query.path : ""; + const relativePath = + typeof req.query.path === "string" ? req.query.path : ""; if (!relativePath.trim()) { res.status(422).json({ error: "Query parameter 'path' is required" }); return; @@ -3768,11 +4536,18 @@ export function agentRoutes( router.patch("/agents/:id", validate(updateAgentSchema), async (req, res) => { const id = req.params.id as string; - const existing = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const existing = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!existing) return; if (hasOwn(req.body as object, "permissions")) { - res.status(422).json({ error: "Use /api/agents/:id/permissions for permission changes" }); + res.status(422).json({ + error: "Use /api/agents/:id/permissions for permission changes", + }); return; } @@ -3791,7 +4566,8 @@ export function agentRoutes( return; } assertNoAgentAdapterConfigMutation(req, adapterConfig); - const changingInstructionsConfig = adapterConfigTouchesInstructionsConfig(adapterConfig); + const changingInstructionsConfig = + adapterConfigTouchesInstructionsConfig(adapterConfig); if (changingInstructionsConfig) { await assertCanManageInstructionsPath(req, existing); } @@ -3804,9 +4580,13 @@ export function agentRoutes( // existing agent uneditable. const requestedAdapterType = hasOwn(patchData, "adapterType") ? (() => { - const next = assertKnownAdapterType(patchData.adapterType as string | null | undefined); - return next === existing.adapterType ? next : assertSelectableAdapterType(next); - })() + const next = assertKnownAdapterType( + patchData.adapterType as string | null | undefined, + ); + return next === existing.adapterType + ? next + : assertSelectableAdapterType(next); + })() : existing.adapterType; let requestedRuntimeConfig: Record | null = null; if (hasOwn(patchData, "runtimeConfig")) { @@ -3816,30 +4596,48 @@ export function agentRoutes( return; } assertNoAgentRuntimeConfigAdapterConfigMutation(req, runtimeConfig); + const existingDebug = asRecord(asRecord(existing.runtimeConfig)?.debug); + const requestedDebug = asRecord(runtimeConfig.debug); + if ( + (existingDebug?.providerTrace === "raw") + !== (requestedDebug?.providerTrace === "raw") + ) { + assertInstanceAdmin(req); + } requestedRuntimeConfig = runtimeConfig; } const touchesAdapterConfiguration = - hasOwn(patchData, "adapterType") || - hasOwn(patchData, "adapterConfig"); + hasOwn(patchData, "adapterType") || hasOwn(patchData, "adapterConfig"); if (touchesAdapterConfiguration) { const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {}; const changingAdapterType = - typeof patchData.adapterType === "string" && patchData.adapterType !== existing.adapterType; + typeof patchData.adapterType === "string" && + patchData.adapterType !== existing.adapterType; const requestedAdapterConfig = hasOwn(patchData, "adapterConfig") ? (asRecord(patchData.adapterConfig) ?? {}) : null; if ( - requestedAdapterConfig - && replaceAdapterConfig - && KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some((key) => - existingAdapterConfig[key] !== undefined && requestedAdapterConfig[key] === undefined, + requestedAdapterConfig && + replaceAdapterConfig && + KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some( + (key) => + existingAdapterConfig[key] !== undefined && + requestedAdapterConfig[key] === undefined, ) ) { await assertCanManageInstructionsPath(req, existing); } - let rawEffectiveAdapterConfig = requestedAdapterConfig ?? existingAdapterConfig; - if (requestedAdapterConfig && !changingAdapterType && !replaceAdapterConfig) { - rawEffectiveAdapterConfig = { ...existingAdapterConfig, ...requestedAdapterConfig }; + let rawEffectiveAdapterConfig = + requestedAdapterConfig ?? existingAdapterConfig; + if ( + requestedAdapterConfig && + !changingAdapterType && + !replaceAdapterConfig + ) { + rawEffectiveAdapterConfig = { + ...existingAdapterConfig, + ...requestedAdapterConfig, + }; } if (changingAdapterType) { // Preserve adapter-agnostic keys (env, cwd, etc.) from the existing config @@ -3847,8 +4645,14 @@ export function agentRoutes( // adapterConfig but omits these keys would silently drop them. for (const key of ADAPTER_AGNOSTIC_KEYS) { if (KNOWN_INSTRUCTIONS_BUNDLE_KEY_SET.has(key)) continue; - if (rawEffectiveAdapterConfig[key] === undefined && existingAdapterConfig[key] !== undefined) { - rawEffectiveAdapterConfig = { ...rawEffectiveAdapterConfig, [key]: existingAdapterConfig[key] }; + if ( + rawEffectiveAdapterConfig[key] === undefined && + existingAdapterConfig[key] !== undefined + ) { + rawEffectiveAdapterConfig = { + ...rawEffectiveAdapterConfig, + [key]: existingAdapterConfig[key], + }; } } rawEffectiveAdapterConfig = preserveInstructionsBundleConfig( @@ -3865,38 +4669,58 @@ export function agentRoutes( rawEffectiveAdapterConfig, ), ); - const normalizedEffectiveAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({ - companyId: existing.companyId, - adapterType: requestedAdapterType, - adapterConfig: effectiveAdapterConfig, - }); - patchData.adapterConfig = syncInstructionsBundleConfigFromFilePath(existing, normalizedEffectiveAdapterConfig); - } - if (requestedRuntimeConfig) { - const baseAdapterConfig = asRecord(patchData.adapterConfig) ?? asRecord(existing.adapterConfig) ?? {}; - patchData.runtimeConfig = await normalizeRuntimeConfigAdapterConfigsForPersistence( - existing.companyId, - requestedAdapterType, - requestedRuntimeConfig, - baseAdapterConfig, + const normalizedEffectiveAdapterConfig = + await normalizeMediatedAdapterConfigForPersistence({ + companyId: existing.companyId, + adapterType: requestedAdapterType, + adapterConfig: effectiveAdapterConfig, + }); + patchData.adapterConfig = syncInstructionsBundleConfigFromFilePath( + existing, + normalizedEffectiveAdapterConfig, ); } - if (touchesAdapterConfiguration || Object.prototype.hasOwnProperty.call(patchData, "defaultEnvironmentId")) { + if (requestedRuntimeConfig) { + const baseAdapterConfig = + asRecord(patchData.adapterConfig) ?? + asRecord(existing.adapterConfig) ?? + {}; + patchData.runtimeConfig = + await normalizeRuntimeConfigAdapterConfigsForPersistence( + existing.companyId, + requestedAdapterType, + requestedRuntimeConfig, + baseAdapterConfig, + ); + } + if ( + touchesAdapterConfiguration || + Object.prototype.hasOwnProperty.call(patchData, "defaultEnvironmentId") + ) { await assertAgentDefaultEnvironmentSelection( existing.companyId, Object.prototype.hasOwnProperty.call(patchData, "defaultEnvironmentId") - ? (typeof patchData.defaultEnvironmentId === "string" ? patchData.defaultEnvironmentId : null) + ? typeof patchData.defaultEnvironmentId === "string" + ? patchData.defaultEnvironmentId + : null : existing.defaultEnvironmentId, { - allowedDrivers: allowedEnvironmentDriversForAgent(requestedAdapterType), - allowedSandboxProviders: allowedSandboxProvidersForAgent(requestedAdapterType), + allowedDrivers: + allowedEnvironmentDriversForAgent(requestedAdapterType), + allowedSandboxProviders: + allowedSandboxProvidersForAgent(requestedAdapterType), }, ); } - const touchesProfileFields = touchesAgentProfileChangeConsentFields(patchData); - const profileOnlyChange = touchesProfileFields && Object.keys(patchData).every((key) => - (AGENT_PROFILE_CHANGE_CONSENT_FIELDS as readonly string[]).includes(key), - ); + const touchesProfileFields = + touchesAgentProfileChangeConsentFields(patchData); + const profileOnlyChange = + touchesProfileFields && + Object.keys(patchData).every((key) => + (AGENT_PROFILE_CHANGE_CONSENT_FIELDS as readonly string[]).includes( + key, + ), + ); if (profileOnlyChange) { await assertCanApplyAgentProfileChange(req, existing); } else { @@ -3911,7 +4735,8 @@ export function agentRoutes( source: "patch", }, claudeLogin: { - ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null), + ownerUserId: + req.actor.type === "agent" ? null : (req.actor.userId ?? null), // The apply-existing path runs only for a user actor. The owner comes // from the actor, so an agent actor never reaches the no-claim bind. applyExistingWithoutClaim: @@ -3974,7 +4799,9 @@ export function agentRoutes( } if (existing.orgChainHealth?.status === "invalid_org_chain") { res.status(409).json({ - error: existing.orgChainHealth?.repairGuidance ?? "Repair this agent's reporting chain before resuming it", + error: + existing.orgChainHealth?.repairGuidance ?? + "Repair this agent's reporting chain before resuming it", }); return; } @@ -4005,7 +4832,9 @@ export function agentRoutes( } if (existing.orgChainHealth?.status === "invalid_org_chain") { res.status(409).json({ - error: existing.orgChainHealth?.repairGuidance ?? "Repair this agent's reporting chain before clearing its error", + error: + existing.orgChainHealth?.repairGuidance ?? + "Repair this agent's reporting chain before clearing its error", }); return; } @@ -4036,7 +4865,9 @@ export function agentRoutes( return; } if (existing.status !== "pending_approval") { - res.status(409).json({ error: "Only pending approval agents can be approved" }); + res + .status(409) + .json({ error: "Only pending approval agents can be approved" }); return; } @@ -4045,7 +4876,10 @@ export function agentRoutes( // hire-approved notification. Fall back to direct activation if no open // approval record exists (e.g. agents created before approvals were tracked). const decidedByUserId = req.actor.userId ?? "board"; - const openApproval = await approvalsSvc.findOpenHireApprovalForAgent(existing.companyId, id); + const openApproval = await approvalsSvc.findOpenHireApprovalForAgent( + existing.companyId, + id, + ); let agent: Awaited> | null = null; if (openApproval) { @@ -4058,7 +4892,9 @@ export function agentRoutes( return; } if (!approval.activated) { - res.status(409).json({ error: "Only pending approval agents can be approved" }); + res + .status(409) + .json({ error: "Only pending approval agents can be approved" }); return; } agent = approval.agent; @@ -4098,7 +4934,10 @@ export function agentRoutes( // reject() already calls agentsSvc.terminate() internally. let agent: Awaited> = null; if (existing.status === "pending_approval") { - const openApproval = await approvalsSvc.findOpenHireApprovalForAgent(existing.companyId, id); + const openApproval = await approvalsSvc.findOpenHireApprovalForAgent( + existing.companyId, + id, + ); if (openApproval) { await approvalsSvc.reject(openApproval.id, req.actor.userId ?? "board"); agent = await svc.getById(id); @@ -4122,7 +4961,10 @@ export function agentRoutes( }) .from(agentsTable) .where(eq(agentsTable.companyId, agent.companyId)); - const invalidOrgChainDescendantIds = listInvalidOrgChainDescendantIds(id, companyAgentRows); + const invalidOrgChainDescendantIds = listInvalidOrgChainDescendantIds( + id, + companyAgentRows, + ); const cancellation = await heartbeat.cancelInvocationsForAgents( [id, ...invalidOrgChainDescendantIds], "Cancelled because the agent was terminated or became invalid-org-chain under a terminated manager", @@ -4139,7 +4981,10 @@ export function agentRoutes( invalidOrgChain: { descendantCount: invalidOrgChainDescendantIds.length, descendantIds: invalidOrgChainDescendantIds, - state: invalidOrgChainDescendantIds.length > 0 ? "descendants_invalid_under_terminated_manager" : "none", + state: + invalidOrgChainDescendantIds.length > 0 + ? "descendants_invalid_under_terminated_manager" + : "none", }, cancellation: { agentIds: cancellation.agentIds, @@ -4187,34 +5032,38 @@ export function agentRoutes( res.json(keys); }); - router.post("/agents/:id/keys", validate(createAgentKeySchema), async (req, res) => { - assertBoard(req); - const id = req.params.id as string; - const agent = await getAccessibleAgent(req, res, id); - if (!agent) { - return; - } - const key = await svc.createApiKey(id, req.body.name, req.body.scope, { - responsibleUserId: req.actor.userId ?? null, - }); + router.post( + "/agents/:id/keys", + validate(createAgentKeySchema), + async (req, res) => { + assertBoard(req); + const id = req.params.id as string; + const agent = await getAccessibleAgent(req, res, id); + if (!agent) { + return; + } + const key = await svc.createApiKey(id, req.body.name, req.body.scope, { + responsibleUserId: req.actor.userId ?? null, + }); - await logActivity(db, { - companyId: agent.companyId, - actorType: "user", - actorId: req.actor.userId ?? "board", - action: "agent.key_created", - entityType: "agent", - entityId: agent.id, - details: { - keyId: key.id, - name: key.name, - scope: key.scope, - responsibleUserId: key.responsibleUserId, - }, - }); + await logActivity(db, { + companyId: agent.companyId, + actorType: "user", + actorId: req.actor.userId ?? "board", + action: "agent.key_created", + entityType: "agent", + entityId: agent.id, + details: { + keyId: key.id, + name: key.name, + scope: key.scope, + responsibleUserId: key.responsibleUserId, + }, + }); - res.status(201).json(key); - }); + res.status(201).json(key); + }, + ); router.delete("/agents/:id/keys/:keyId", async (req, res) => { assertBoard(req); @@ -4262,7 +5111,9 @@ export function agentRoutes( type HeartbeatSource = "timer" | "assignment" | "on_demand" | "automation"; type WakeupRouteOpts = { source: HeartbeatSource | undefined; - skippedResponse: (agent: NonNullable>>) => unknown | Promise; + skippedResponse: ( + agent: NonNullable>>, + ) => unknown | Promise; }; const handleWakeupRoute = async ( req: Request, @@ -4270,7 +5121,12 @@ export function agentRoutes( opts: WakeupRouteOpts, ): Promise => { const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!agent) return; if (req.actor.type === "agent") { @@ -4281,9 +5137,14 @@ export function agentRoutes( } else { await assertBoardCanManageAgentsForCompany(req, agent.companyId); } + if (req.body.debug?.providerTrace === "raw") { + assertInstanceAdmin(req); + } if (agent.orgChainHealth?.status === "invalid_org_chain") { res.status(409).json({ - error: agent.orgChainHealth?.repairGuidance ?? "Repair this agent's reporting chain before starting runs", + error: + agent.orgChainHealth?.repairGuidance ?? + "Repair this agent's reporting chain before starting runs", }); return; } @@ -4295,11 +5156,25 @@ export function agentRoutes( payload: req.body.payload ?? null, idempotencyKey: req.body.idempotencyKey ?? null, requestedByActorType: req.actor.type === "agent" ? "agent" : "user", - requestedByActorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null, + requestedByActorId: + req.actor.type === "agent" + ? (req.actor.agentId ?? null) + : (req.actor.userId ?? null), contextSnapshot: { triggeredBy: req.actor.type, - actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId, + actorId: + req.actor.type === "agent" ? req.actor.agentId : req.actor.userId, forceFreshSession: req.body.forceFreshSession === true, + ...(req.body.reason === "rerun_with_provider_trace" && + req.body.debug?.providerTrace === "raw" + ? { resumeIntent: true } + : {}), + ...(req.body.debug?.providerTrace === "raw" + ? { + debug: { providerTrace: "raw" }, + providerTraceRequestedBy: req.actor.userId ?? "local-admin", + } + : {}), }, }); @@ -4320,16 +5195,38 @@ export function agentRoutes( entityId: run.id, details: { agentId: id }, }); + if (req.body.debug?.providerTrace === "raw") { + await logActivity(db, { + companyId: agent.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: run.id, + action: "provider_trace.capture_requested", + entityType: "heartbeat_run", + entityId: run.id, + details: { + mode: "raw", + retentionHours: 24, + maxBytes: 64 * 1024 * 1024, + }, + }); + } res.status(202).json(run); }; - router.post("/agents/:id/wakeup", validate(wakeAgentSchema), async (req, res) => { - await handleWakeupRoute(req, res, { - source: req.body.source, - skippedResponse: (agent) => buildSkippedWakeupResponse(agent, req.body.payload ?? null), - }); - }); + router.post( + "/agents/:id/wakeup", + validate(wakeAgentSchema), + async (req, res) => { + await handleWakeupRoute(req, res, { + source: req.body.source, + skippedResponse: (agent) => + buildSkippedWakeupResponse(agent, req.body.payload ?? null), + }); + }, + ); router.post("/agents/:id/heartbeat/invoke", async (req, res) => { // Legacy endpoint. Hardcodes `source: "on_demand"` (the prior behavior @@ -4340,7 +5237,12 @@ export function agentRoutes( // an empty body produces the original fixed-arg `heartbeat.invoke()` // shape exactly. const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!agent) return; if (req.actor.type === "agent") { @@ -4351,9 +5253,15 @@ export function agentRoutes( } else { await assertBoardCanManageAgentsForCompany(req, agent.companyId); } + const providerTraceRequested = req.body?.debug?.providerTrace === "raw"; + if (providerTraceRequested) { + assertInstanceAdmin(req); + } if (agent.orgChainHealth?.status === "invalid_org_chain") { res.status(409).json({ - error: agent.orgChainHealth?.repairGuidance ?? "Repair this agent's reporting chain before starting runs", + error: + agent.orgChainHealth?.repairGuidance ?? + "Repair this agent's reporting chain before starting runs", }); return; } @@ -4364,28 +5272,51 @@ export function agentRoutes( idempotencyKey: unknown; forceFreshSession: unknown; triggerDetail: unknown; + debug: unknown; }>; const contextSnapshot: Record = { triggeredBy: req.actor.type, - actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId, + actorId: + req.actor.type === "agent" ? req.actor.agentId : req.actor.userId, }; if (body.forceFreshSession === true) { contextSnapshot.forceFreshSession = true; } + if (providerTraceRequested) { + contextSnapshot.debug = { providerTrace: "raw" }; + contextSnapshot.providerTraceRequestedBy = + req.actor.userId ?? "local-admin"; + if (body.reason === "rerun_with_provider_trace") { + contextSnapshot.resumeIntent = true; + } + } const wakeOpts: Parameters[1] = { source: "on_demand", - triggerDetail: typeof body.triggerDetail === "string" ? body.triggerDetail as "manual" | "system" | "ping" | "callback" : "manual", + triggerDetail: + typeof body.triggerDetail === "string" + ? (body.triggerDetail as "manual" | "system" | "ping" | "callback") + : "manual", requestedByActorType: req.actor.type === "agent" ? "agent" : "user", - requestedByActorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null, + requestedByActorId: + req.actor.type === "agent" + ? (req.actor.agentId ?? null) + : (req.actor.userId ?? null), contextSnapshot, }; if (typeof body.reason === "string" && body.reason.length > 0) { wakeOpts.reason = body.reason; } - if (body.payload && typeof body.payload === "object" && !Array.isArray(body.payload)) { + if ( + body.payload && + typeof body.payload === "object" && + !Array.isArray(body.payload) + ) { wakeOpts.payload = body.payload as Record; } - if (typeof body.idempotencyKey === "string" && body.idempotencyKey.length > 0) { + if ( + typeof body.idempotencyKey === "string" && + body.idempotencyKey.length > 0 + ) { wakeOpts.idempotencyKey = body.idempotencyKey; } const run = await heartbeat.wakeup(id, wakeOpts); @@ -4407,6 +5338,23 @@ export function agentRoutes( entityId: run.id, details: { agentId: id }, }); + if (providerTraceRequested) { + await logActivity(db, { + companyId: agent.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: run.id, + action: "provider_trace.capture_requested", + entityType: "heartbeat_run", + entityId: run.id, + details: { + mode: "raw", + retentionHours: 24, + maxBytes: 64 * 1024 * 1024, + }, + }); + } res.status(202).json(run); }); @@ -4414,23 +5362,34 @@ export function agentRoutes( router.post("/agents/:id/claude-login", async (req, res) => { assertBoard(req); const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); + const agent = await getAccessibleResource( + req, + res, + svc.getById(id), + "Agent not found", + ); if (!agent) return; await assertBoardCanManageAgentsForCompany(req, agent.companyId); if (agent.adapterType !== "claude_local") { - res.status(400).json({ error: "Login is only supported for claude_local agents" }); + res + .status(400) + .json({ error: "Login is only supported for claude_local agents" }); return; } const config = asRecord(agent.adapterConfig) ?? {}; // Persisted agent: default declared mode; consumerId = agent.id matches the // declaration rows written at env. by syncAgentAdapterEnvBindings. - const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime( - agent.companyId, - config, - buildActorSecretContext(req, { consumerType: "agent", consumerId: agent.id }), - { adapterType: agent.adapterType }, - ); + const { config: runtimeConfig } = + await secretsSvc.resolveAdapterConfigForRuntime( + agent.companyId, + config, + buildActorSecretContext(req, { + consumerType: "agent", + consumerId: agent.id, + }), + { adapterType: agent.adapterType }, + ); const result = await runClaudeLogin({ runId: `claude-login-${randomUUID()}`, agent: { @@ -4527,15 +5486,27 @@ export function agentRoutes( * the transport is confidential (direct TLS, a local-trusted loopback, or an * allowlisted TLS proxy), so a confidential response shows no disclaimer. */ - const assessSetupTokenTransport = (req: Request): SetupTokenTransportAdvisory | null => { - const socket = req.socket as { encrypted?: boolean; remoteAddress?: string }; + const assessSetupTokenTransport = ( + req: Request, + ): SetupTokenTransportAdvisory | null => { + const socket = req.socket as { + encrypted?: boolean; + remoteAddress?: string; + }; const forwardedProto = req.headers["x-forwarded-proto"]; - const decision = evaluateConfidentialTransport(setupTokenConfidentialConfig, { - socketEncrypted: socket?.encrypted === true, - remoteAddress: socket?.remoteAddress, - forwardedProto: Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto, - }); - return decision.allowed ? null : { code: SETUP_TOKEN_TRANSPORT_ADVISORY_CODE }; + const decision = evaluateConfidentialTransport( + setupTokenConfidentialConfig, + { + socketEncrypted: socket?.encrypted === true, + remoteAddress: socket?.remoteAddress, + forwardedProto: Array.isArray(forwardedProto) + ? forwardedProto[0] + : forwardedProto, + }, + ); + return decision.allowed + ? null + : { code: SETUP_TOKEN_TRANSPORT_ADVISORY_CODE }; }; const sendSetupTokenError = (res: Response, err: unknown): void => { @@ -4568,7 +5539,9 @@ export function agentRoutes( // Maps the internal session state to the public login status. The public union // carries no server-only state, so the route never returns the internal // `submitting` or `stored` state to a client. - const toClaudeLoginStatus = (state: SetupTokenSessionState): AdapterAuthSessionStatus => { + const toClaudeLoginStatus = ( + state: SetupTokenSessionState, + ): AdapterAuthSessionStatus => { switch (state) { case "starting": return "starting"; @@ -4590,7 +5563,9 @@ export function agentRoutes( // Builds the fixed, non-secret failure for a terminal failure state. A live or // a completed session has no failure. The failure carries a stable reason and // no secret detail. - const toClaudeLoginFailure = (state: SetupTokenSessionState): AdapterAuthSessionFailure | null => { + const toClaudeLoginFailure = ( + state: SetupTokenSessionState, + ): AdapterAuthSessionFailure | null => { switch (state) { case "failed": return { reason: "failed", message: null }; @@ -4634,264 +5609,345 @@ export function agentRoutes( // company gate returns for a non-member, so it discloses no existence // distinction across owners or companies. It sets `Cache-Control: no-store`, // so no cache holds the metadata. - router.get("/companies/:companyId/claude-oauth-token-status", async (req, res) => { - const companyId = req.params.companyId as string; - const ownerUserId = resolveCompanySessionOwner(req, companyId, res); - if (ownerUserId === null) return; - res.setHeader("Cache-Control", "no-store"); - const status = await secretsSvc.readClaudeOAuthUserSecretStatus(companyId, ownerUserId); - if (!status) { - // A missing owner value returns the same fixed not-found as the non-member - // gate, so a member without a value and a non-member look the same. - res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND }); - return; - } - const body: ClaudeOAuthTokenStatusResponse = status; - res.json(body); - }); - - router.post("/companies/:companyId/setup-token-login-sessions", async (req, res) => { - const companyId = req.params.companyId as string; - - // The shared start-route spine derives the owner, validates the strict - // request schema, runs the Claude-only guards, and checks the sandbox - // environment before any session, lease, or pseudo-terminal side effect. - // - // The owner step runs the company access check, derives the owner, and then - // sets `Cache-Control: no-store`, so a rejected member sees no cache header - // and every other response carries it. The strict schema rejects an unknown - // field, including a legacy `ttlSeconds`, with a fixed 400. The post-validate - // guard rejects a non-Claude adapter with a fixed 400 and fails closed with - // the fixed no-secret 503 until the live login transport binds. The sandbox - // check fails closed on a missing, archived, non-sandbox, fake-provider, or - // foreign environment, and on a provider without the setup-token login - // capability, so no rejected environment reaches a session row, a lease, or a - // pseudo-terminal. - const resolved = await runAdapterLoginStartSpine({ - req, - res, - deriveOwner: () => { - assertCompanyAccess(req, companyId); - const ownerUserId = deriveSetupTokenOwnerUserId(req); - res.setHeader("Cache-Control", "no-store"); - return ownerUserId; - }, - requestSchema: startClaudeSetupTokenSessionRequestSchema, - invalidRequestError: "The Claude login start request is invalid.", - guardAfterValidate: (data) => { - // The setup-token route drives a login on a pseudo-terminal and records a - // stored session identifier on success. It serves any adapter whose - // registry login capability declares that transport and that claim. The - // guard reads the capability, not the adapter name, so a new adapter with - // the same capability passes with no code change. It rejects an adapter - // with no matching capability with a fixed 400. - const capability = getRegistryLoginCapability(data.adapterType); - if ( - capability?.sandboxTransport !== "pseudo_terminal" || - capability.completionClaim !== "storedSessionId" - ) { - res.status(400).json({ error: "This adapter does not support a setup-token login." }); - return true; - } - if (!SETUP_TOKEN_LOGIN_TRANSPORT_READY) { - res.status(503).json({ error: SETUP_TOKEN_START_FAILED }); - return true; - } - return false; - }, - assertSandbox: (data) => - assertSandboxLoginEnvironment(companyId, data.environmentId, { - requireSetupTokenLoginProvider: true, - }), - }); - if (!resolved) return; - const { ownerUserId, data } = resolved; - const { environmentId, adapterType } = data; - const confirmedOverwrite: ClaudeSetupTokenOverwrite | null = data.overwrite ?? null; - - const scope: SetupTokenSessionScope = { - companyId, - ownerUserId, - adapterType, - environmentId, - confirmedOverwrite, - }; - // Read the panel mode from the adapter capability. The guard already checked - // the capability, so it is present here. The client renders the panel from - // this value instead of a hard-coded mode. - const panelMode = - getRegistryLoginCapability(adapterType)?.panelMode ?? "submitted_browser_code"; - try { - const started = await setupTokenLoginService.start(scope); - const descriptor = setupTokenLoginService.describeOwned(started.sessionId, scope); - // The start response carries the panel mode, so the client renders the - // correct panel. The full login URL rides only through the guarded prompt - // read, not the start response, so the prompt is null here. The client - // reads the prompt route for the login URL. - const body: ClaudeSetupTokenSessionOwnerResponse = { - ...toClaudePublicResponse(descriptor), - panelMode, - prompt: null, - }; - res.status(201).json(body); - } catch (err) { - sendSetupTokenError(res, err); - } - }); - - router.get("/companies/:companyId/setup-token-login-sessions/:sessionId", async (req, res) => { - const companyId = req.params.companyId as string; - const ownerUserId = resolveCompanySessionOwner(req, companyId, res); - if (ownerUserId === null) return; - res.setHeader("Cache-Control", "no-store"); - try { - const sessionId = req.params.sessionId as string; - const scope = setupTokenLoginService.resolveCompanyScope( - sessionId, - companySetupTokenKey(companyId, ownerUserId), + router.get( + "/companies/:companyId/claude-oauth-token-status", + async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; + res.setHeader("Cache-Control", "no-store"); + const status = await secretsSvc.readClaudeOAuthUserSecretStatus( + companyId, + ownerUserId, ); - const descriptor = setupTokenLoginService.describeOwned(sessionId, scope); - // The status response is public. It carries no prompt and no secret. - res.json(toClaudePublicResponse(descriptor)); - } catch (err) { - sendSetupTokenError(res, err); - } - }); - - router.get("/companies/:companyId/setup-token-login-sessions/:sessionId/prompt", async (req, res) => { - const companyId = req.params.companyId as string; - const ownerUserId = resolveCompanySessionOwner(req, companyId, res); - if (ownerUserId === null) return; - res.setHeader("Cache-Control", "no-store"); - // The full login URL is a confidential response. The route - // does not force TLS. It attaches a non-blocking advisory instead. - const transportAdvisory = assessSetupTokenTransport(req); - try { - const sessionId = req.params.sessionId as string; - const scope = setupTokenLoginService.resolveCompanyScope( - sessionId, - companySetupTokenKey(companyId, ownerUserId), - ); - const descriptor = setupTokenLoginService.describeOwned(sessionId, scope); - if (!descriptor.loginUrl) { - // The prompt has not surfaced yet. Return the same not-found error as a - // missing or a foreign session, so the route never confirms the session - // exists before the URL is ready. + if (!status) { + // A missing owner value returns the same fixed not-found as the non-member + // gate, so a member without a value and a non-member look the same. res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND }); return; } - // The full login URL rides only in this authorized owner response. - const body: ClaudeSetupTokenSessionPrompt = { - authorizationUrl: descriptor.loginUrl, - transportAdvisory, - }; + const body: ClaudeOAuthTokenStatusResponse = status; res.json(body); - } catch (err) { - sendSetupTokenError(res, err); - } - }); + }, + ); - router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/code", async (req, res) => { - const companyId = req.params.companyId as string; - const ownerUserId = resolveCompanySessionOwner(req, companyId, res); - if (ownerUserId === null) return; - res.setHeader("Cache-Control", "no-store"); - // The browser code is the confidential OAuth authorization - // secret. The route does not force TLS. It attaches a non-blocking advisory - // to the response instead, so the client can show a disclaimer. - const transportAdvisory = assessSetupTokenTransport(req); - // Parse the request with the shared strict validator before the route forwards - // the code to the live process. `.strict()` rejects an unknown field, and the - // grammar rejects an empty, an oversized, or a control-byte code. The route - // echoes no input; it returns fixed error text only. - const parsed = submitBrowserCodeRequestSchema.safeParse(req.body); - if (!parsed.success) { - res.status(400).json({ error: "A valid browser code is required." }); - return; - } - try { - const sessionId = req.params.sessionId as string; - const scope = setupTokenLoginService.resolveCompanyScope( - sessionId, - companySetupTokenKey(companyId, ownerUserId), - ); - setupTokenLoginService.submitCode(sessionId, scope, parsed.data.browserCode); - const descriptor = setupTokenLoginService.describeOwned(sessionId, scope); - const body: ClaudeSetupTokenSessionResponse = { - ...toClaudePublicResponse(descriptor), - transportAdvisory, - }; - res.json(body); - } catch (err) { - sendSetupTokenError(res, err); - } - }); + router.post( + "/companies/:companyId/setup-token-login-sessions", + async (req, res) => { + const companyId = req.params.companyId as string; - router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/completion", async (req, res) => { - const companyId = req.params.companyId as string; - const ownerUserId = resolveCompanySessionOwner(req, companyId, res); - if (ownerUserId === null) return; - res.setHeader("Cache-Control", "no-store"); - try { - const sessionId = req.params.sessionId as string; - const scope = setupTokenLoginService.resolveCompanyScope( - sessionId, - companySetupTokenKey(companyId, ownerUserId), - ); - // The service returns the non-secret `storedSessionId` claim from a - // completed session whose owner-bound secret write succeeded. The response - // carries no token. - const result = setupTokenLoginService.completeSession(sessionId, scope); - const body: ClaudeSetupTokenCompletionResponse = { storedSessionId: result.storedSessionId }; - res.json(body); - } catch (err) { - sendSetupTokenError(res, err); - } - }); - - router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/cancel", async (req, res) => { - const companyId = req.params.companyId as string; - const ownerUserId = resolveCompanySessionOwner(req, companyId, res); - if (ownerUserId === null) return; - res.setHeader("Cache-Control", "no-store"); - const sessionId = req.params.sessionId as string; - try { - const scope = setupTokenLoginService.resolveCompanyScope( - sessionId, - companySetupTokenKey(companyId, ownerUserId), - ); - await setupTokenLoginService.cancel(sessionId, scope); - res.status(200).json({}); - } catch (err) { - // Cancel is idempotent. The service removes a session when it reaches a - // terminal state, so a repeat cancel, a cancel after a timeout, or a - // cancel of an unknown session finds no record and throws the fixed - // not-found error. Return the same success as an active cancel, so the - // client stops the poll and returns to its start state. + // The shared start-route spine derives the owner, validates the strict + // request schema, runs the Claude-only guards, and checks the sandbox + // environment before any session, lease, or pseudo-terminal side effect. // - // This keeps the not-found uniform. The 200 response is identical - // for a missing session, an already-terminal session, and a foreign - // session, so the route never confirms a session exists and cancels - // nothing for a foreign id. A non-member still fails closed with a 404 at - // the company-access gate above, before this handler runs. A non-404 - // error still surfaces. - if (err instanceof SetupTokenSessionError && err.status === 404) { - res.status(200).json({}); + // The owner step runs the company access check, derives the owner, and then + // sets `Cache-Control: no-store`, so a rejected member sees no cache header + // and every other response carries it. The strict schema rejects an unknown + // field, including a legacy `ttlSeconds`, with a fixed 400. The post-validate + // guard rejects a non-Claude adapter with a fixed 400 and fails closed with + // the fixed no-secret 503 until the live login transport binds. The sandbox + // check fails closed on a missing, archived, non-sandbox, fake-provider, or + // foreign environment, and on a provider without the setup-token login + // capability, so no rejected environment reaches a session row, a lease, or a + // pseudo-terminal. + const resolved = await runAdapterLoginStartSpine({ + req, + res, + deriveOwner: () => { + assertCompanyAccess(req, companyId); + const ownerUserId = deriveSetupTokenOwnerUserId(req); + res.setHeader("Cache-Control", "no-store"); + return ownerUserId; + }, + requestSchema: startClaudeSetupTokenSessionRequestSchema, + invalidRequestError: "The Claude login start request is invalid.", + guardAfterValidate: (data) => { + // The setup-token route drives a login on a pseudo-terminal and records a + // stored session identifier on success. It serves any adapter whose + // registry login capability declares that transport and that claim. The + // guard reads the capability, not the adapter name, so a new adapter with + // the same capability passes with no code change. It rejects an adapter + // with no matching capability with a fixed 400. + const capability = getRegistryLoginCapability(data.adapterType); + if ( + capability?.sandboxTransport !== "pseudo_terminal" || + capability.completionClaim !== "storedSessionId" + ) { + res.status(400).json({ + error: "This adapter does not support a setup-token login.", + }); + return true; + } + if (!SETUP_TOKEN_LOGIN_TRANSPORT_READY) { + res.status(503).json({ error: SETUP_TOKEN_START_FAILED }); + return true; + } + return false; + }, + assertSandbox: (data) => + assertSandboxLoginEnvironment(companyId, data.environmentId, { + requireSetupTokenLoginProvider: true, + }), + }); + if (!resolved) return; + const { ownerUserId, data } = resolved; + const { environmentId, adapterType } = data; + const confirmedOverwrite: ClaudeSetupTokenOverwrite | null = + data.overwrite ?? null; + + const scope: SetupTokenSessionScope = { + companyId, + ownerUserId, + adapterType, + environmentId, + confirmedOverwrite, + }; + // Read the panel mode from the adapter capability. The guard already checked + // the capability, so it is present here. The client renders the panel from + // this value instead of a hard-coded mode. + const panelMode = + getRegistryLoginCapability(adapterType)?.panelMode ?? + "submitted_browser_code"; + try { + const started = await setupTokenLoginService.start(scope); + const descriptor = setupTokenLoginService.describeOwned( + started.sessionId, + scope, + ); + // The start response carries the panel mode, so the client renders the + // correct panel. The full login URL rides only through the guarded prompt + // read, not the start response, so the prompt is null here. The client + // reads the prompt route for the login URL. + const body: ClaudeSetupTokenSessionOwnerResponse = { + ...toClaudePublicResponse(descriptor), + panelMode, + prompt: null, + }; + res.status(201).json(body); + } catch (err) { + sendSetupTokenError(res, err); + } + }, + ); + + router.get( + "/companies/:companyId/setup-token-login-sessions/:sessionId", + async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; + res.setHeader("Cache-Control", "no-store"); + try { + const sessionId = req.params.sessionId as string; + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + const descriptor = setupTokenLoginService.describeOwned( + sessionId, + scope, + ); + // The status response is public. It carries no prompt and no secret. + res.json(toClaudePublicResponse(descriptor)); + } catch (err) { + sendSetupTokenError(res, err); + } + }, + ); + + router.get( + "/companies/:companyId/setup-token-login-sessions/:sessionId/prompt", + async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; + res.setHeader("Cache-Control", "no-store"); + // The full login URL is a confidential response. The route + // does not force TLS. It attaches a non-blocking advisory instead. + const transportAdvisory = assessSetupTokenTransport(req); + try { + const sessionId = req.params.sessionId as string; + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + const descriptor = setupTokenLoginService.describeOwned( + sessionId, + scope, + ); + if (!descriptor.loginUrl) { + // The prompt has not surfaced yet. Return the same not-found error as a + // missing or a foreign session, so the route never confirms the session + // exists before the URL is ready. + res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND }); + return; + } + // The full login URL rides only in this authorized owner response. + const body: ClaudeSetupTokenSessionPrompt = { + authorizationUrl: descriptor.loginUrl, + transportAdvisory, + }; + res.json(body); + } catch (err) { + sendSetupTokenError(res, err); + } + }, + ); + + router.post( + "/companies/:companyId/setup-token-login-sessions/:sessionId/code", + async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; + res.setHeader("Cache-Control", "no-store"); + // The browser code is the confidential OAuth authorization + // secret. The route does not force TLS. It attaches a non-blocking advisory + // to the response instead, so the client can show a disclaimer. + const transportAdvisory = assessSetupTokenTransport(req); + // Parse the request with the shared strict validator before the route forwards + // the code to the live process. `.strict()` rejects an unknown field, and the + // grammar rejects an empty, an oversized, or a control-byte code. The route + // echoes no input; it returns fixed error text only. + const parsed = submitBrowserCodeRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: "A valid browser code is required." }); return; } - sendSetupTokenError(res, err); - } - }); + try { + const sessionId = req.params.sessionId as string; + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + setupTokenLoginService.submitCode( + sessionId, + scope, + parsed.data.browserCode, + ); + const descriptor = setupTokenLoginService.describeOwned( + sessionId, + scope, + ); + const body: ClaudeSetupTokenSessionResponse = { + ...toClaudePublicResponse(descriptor), + transportAdvisory, + }; + res.json(body); + } catch (err) { + sendSetupTokenError(res, err); + } + }, + ); + + router.post( + "/companies/:companyId/setup-token-login-sessions/:sessionId/completion", + async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; + res.setHeader("Cache-Control", "no-store"); + try { + const sessionId = req.params.sessionId as string; + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + // The service returns the non-secret `storedSessionId` claim from a + // completed session whose owner-bound secret write succeeded. The response + // carries no token. + const result = setupTokenLoginService.completeSession(sessionId, scope); + const body: ClaudeSetupTokenCompletionResponse = { + storedSessionId: result.storedSessionId, + }; + res.json(body); + } catch (err) { + sendSetupTokenError(res, err); + } + }, + ); + + router.post( + "/companies/:companyId/setup-token-login-sessions/:sessionId/cancel", + async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; + res.setHeader("Cache-Control", "no-store"); + const sessionId = req.params.sessionId as string; + try { + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + await setupTokenLoginService.cancel(sessionId, scope); + res.status(200).json({}); + } catch (err) { + // Cancel is idempotent. The service removes a session when it reaches a + // terminal state, so a repeat cancel, a cancel after a timeout, or a + // cancel of an unknown session finds no record and throws the fixed + // not-found error. Return the same success as an active cancel, so the + // client stops the poll and returns to its start state. + // + // This keeps the not-found uniform. The 200 response is identical + // for a missing session, an already-terminal session, and a foreign + // session, so the route never confirms a session exists and cancels + // nothing for a foreign id. A non-member still fails closed with a 404 at + // the company-access gate above, before this handler runs. A non-404 + // error still surfaces. + if (err instanceof SetupTokenSessionError && err.status === 404) { + res.status(200).json({}); + return; + } + sendSetupTokenError(res, err); + } + }, + ); router.get("/companies/:companyId/heartbeat-runs", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const agentId = req.query.agentId as string | undefined; const limitParam = req.query.limit as string | undefined; - const limit = limitParam ? Math.max(1, Math.min(1000, parseInt(limitParam, 10) || 200)) : undefined; + const limit = limitParam + ? Math.max(1, Math.min(1000, parseInt(limitParam, 10) || 200)) + : undefined; const summary = req.query.summary === "true" || req.query.summary === "1"; const runs = await heartbeat.list(companyId, agentId, limit, { summary }); - res.json(await Promise.all(runs.map((run) => runRedactions.redactForRun(companyId, run.id, run)))); + res.json( + await Promise.all( + runs.map((run) => runRedactions.redactForRun(companyId, run.id, run)), + ), + ); + }); + + router.get("/companies/:companyId/provider-traces", async (req, res) => { + assertInstanceAdmin(req); + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const runIds = String(req.query.runIds ?? "") + .split(",") + .map((value) => value.trim()) + .filter(Boolean) + .slice(0, 100); + const traces = await providerTraces.listMetadataForRuns(companyId, runIds); + const actor = getActorInfo(req); + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + action: "provider_trace.metadata_listed", + entityType: "company", + entityId: companyId, + details: { + requestedRunCount: runIds.length, + traceCount: traces.length, + payloadLogged: false, + }, + }); + res.set("Cache-Control", "no-cache, no-store"); + res.json(traces); }); router.get("/companies/:companyId/live-runs", async (req, res) => { @@ -4912,8 +5968,16 @@ export function agentRoutes( status: heartbeatRuns.status, invocationSource: heartbeatRuns.invocationSource, triggerDetail: heartbeatRuns.triggerDetail, - contextCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"), - contextWakeCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"), + contextCommentId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as( + "contextCommentId", + ), + contextWakeCommentId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as( + "contextWakeCommentId", + ), startedAt: heartbeatRuns.startedAt, finishedAt: heartbeatRuns.finishedAt, createdAt: heartbeatRuns.createdAt, @@ -4931,7 +5995,9 @@ export function agentRoutes( lastOutputStream: heartbeatRuns.lastOutputStream, lastOutputBytes: heartbeatRuns.lastOutputBytes, processStartedAt: heartbeatRuns.processStartedAt, - issueId: sql`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"), + issueId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"), }; const liveRunsQuery = db @@ -4959,56 +6025,90 @@ export function agentRoutes( and( eq(heartbeatRuns.companyId, companyId), not(inArray(heartbeatRuns.status, ["queued", "running"])), - ...(activeIds.length > 0 ? [not(inArray(heartbeatRuns.id, activeIds))] : []), + ...(activeIds.length > 0 + ? [not(inArray(heartbeatRuns.id, activeIds))] + : []), ), ) .orderBy(desc(heartbeatRuns.createdAt)) .limit(targetRunCount - liveRuns.length); const rows = [...liveRuns, ...recentRuns]; - res.json(await Promise.all(rows.map(async (run) => runRedactions.redactForRun(companyId, run.id, { - ...heartbeat.decorateActiveRunStatus(run), - outputSilence: await heartbeat.buildRunOutputSilence(run), - })))); + res.json( + await Promise.all( + rows.map(async (run) => + runRedactions.redactForRun(companyId, run.id, { + ...heartbeat.decorateActiveRunStatus(run), + outputSilence: await heartbeat.buildRunOutputSilence(run), + }), + ), + ), + ); return; } - res.json(await Promise.all(liveRuns.map(async (run) => runRedactions.redactForRun(companyId, run.id, { - ...heartbeat.decorateActiveRunStatus(run), - outputSilence: await heartbeat.buildRunOutputSilence(run), - })))); + res.json( + await Promise.all( + liveRuns.map(async (run) => + runRedactions.redactForRun(companyId, run.id, { + ...heartbeat.decorateActiveRunStatus(run), + outputSilence: await heartbeat.buildRunOutputSilence(run), + }), + ), + ), + ); }); router.get("/heartbeat-runs/:runId", async (req, res) => { const runId = req.params.runId as string; - const run = await getAccessibleResource(req, res, heartbeat.getRun(runId), "Heartbeat run not found"); + const run = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); if (!run) return; const retryExhaustedReason = await heartbeat.getRetryExhaustedReason(runId); const decoratedRun = heartbeat.decorateActiveRunStatus(run); - res.json(await runRedactions.redactForRun( - run.companyId, - run.id, - redactCurrentUserValue( - { ...decoratedRun, retryExhaustedReason, outputSilence: await heartbeat.buildRunOutputSilence(run) }, - await getCurrentUserRedactionOptions(), + res.json( + await runRedactions.redactForRun( + run.companyId, + run.id, + redactCurrentUserValue( + { + ...decoratedRun, + retryExhaustedReason, + outputSilence: await heartbeat.buildRunOutputSilence(run), + }, + await getCurrentUserRedactionOptions(), + ), ), - )); + ); }); router.post("/heartbeat-runs/:runId/cancel", async (req, res) => { assertBoard(req); const runId = req.params.runId as string; - const existing = await getAccessibleResource(req, res, heartbeat.getRun(runId), "Heartbeat run not found"); + const existing = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); if (!existing) return; // Stamp the cancellation as operator-initiated (this route is board-only). // Recovery reads this to stand down instead of classifying the cancelled // run as agent stranding and re-waking the agent the operator just stopped. - const run = await heartbeat.cancelRun(runId, "Cancelled by a board operator", { - resultJson: { - cancelledByActorType: "user", - cancelledByUserId: req.actor.userId ?? null, + const run = await heartbeat.cancelRun( + runId, + "Cancelled by a board operator", + { + resultJson: { + cancelledByActorType: "user", + cancelledByUserId: req.actor.userId ?? null, + }, }, - }); + ); if (run) { await logActivity(db, { @@ -5025,86 +6125,131 @@ export function agentRoutes( res.json(run); }); - router.post("/heartbeat-runs/:runId/runtime-requests/:requestId/resolve", async (req, res) => { - assertBoard(req); - const runId = req.params.runId as string; - const requestId = req.params.requestId as string; - const existing = await getAccessibleResource(req, res, heartbeat.getRun(runId), "Heartbeat run not found"); - if (!existing) return; - if (existing.runtimeMode !== "native" || existing.status !== "running") { - throw conflict("This runner session is no longer accepting runtime responses."); - } - const turnId = typeof req.body?.turnId === "string" ? req.body.turnId.trim() : ""; - const rawRequestKind = typeof req.body?.requestKind === "string" ? req.body.requestKind : ""; - const runtimeRequestKinds: readonly HarnessRuntimeRequestKind[] = [ - "command_approval", - "file_approval", - "permission_approval", - "user_input", - "elicitation", - ]; - if (!requestId || requestId.length > 160 || !turnId || turnId.length > 160) { - throw badRequest("Runtime request and turn identifiers are required."); - } - if (!runtimeRequestKinds.includes(rawRequestKind as HarnessRuntimeRequestKind)) { - throw badRequest("Unsupported runtime request kind."); - } - let resolution: HarnessRuntimeRequestResolution; - try { - resolution = parseHarnessRuntimeRequestResolution( - rawRequestKind as HarnessRuntimeRequestKind, - req.body?.resolution, + router.post( + "/heartbeat-runs/:runId/runtime-requests/:requestId/resolve", + async (req, res) => { + assertBoard(req); + const runId = req.params.runId as string; + const requestId = req.params.requestId as string; + const existing = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", ); - } catch (error) { - if (error instanceof HarnessRuntimeRequestResolutionError) { - throw badRequest("Invalid runtime request response."); + if (!existing) return; + if (existing.runtimeMode !== "native" || existing.status !== "running") { + throw conflict( + "This runner session is no longer accepting runtime responses.", + ); + } + const turnId = + typeof req.body?.turnId === "string" ? req.body.turnId.trim() : ""; + const rawRequestKind = + typeof req.body?.requestKind === "string" ? req.body.requestKind : ""; + const runtimeRequestKinds: readonly HarnessRuntimeRequestKind[] = [ + "command_approval", + "file_approval", + "permission_approval", + "user_input", + "elicitation", + ]; + if ( + !requestId || + requestId.length > 160 || + !turnId || + turnId.length > 160 + ) { + throw badRequest("Runtime request and turn identifiers are required."); + } + if ( + !runtimeRequestKinds.includes( + rawRequestKind as HarnessRuntimeRequestKind, + ) + ) { + throw badRequest("Unsupported runtime request kind."); + } + let resolution: HarnessRuntimeRequestResolution; + try { + resolution = parseHarnessRuntimeRequestResolution( + rawRequestKind as HarnessRuntimeRequestKind, + req.body?.resolution, + ); + } catch (error) { + if (error instanceof HarnessRuntimeRequestResolutionError) { + throw badRequest("Invalid runtime request response."); + } + throw error; } - throw error; - } - try { - const queued = queueRunnerPrpRuntimeRequestResolution({ - runId, - requestId, - turnId, - resolution, - }); - await logActivity(db, { - companyId: existing.companyId, - actorType: "user", - actorId: req.actor.userId ?? "board", - action: "heartbeat.runtime_request_resolution_queued", - entityType: "heartbeat_run", - entityId: existing.id, - details: { requestId, action: resolution.action }, - }); - res.status(202).json({ accepted: true, commandId: queued.commandId }); - } catch (error) { - if (error instanceof RunnerPrpRuntimeRequestResolutionError) { - throw conflict(error.code === "runtime_request_resolution_conflict" - ? "A different response was already submitted for this runtime request." - : "The runner session is no longer accepting runtime responses."); + try { + const queued = queueRunnerPrpRuntimeRequestResolution({ + runId, + requestId, + turnId, + resolution, + }); + await logActivity(db, { + companyId: existing.companyId, + actorType: "user", + actorId: req.actor.userId ?? "board", + action: "heartbeat.runtime_request_resolution_queued", + entityType: "heartbeat_run", + entityId: existing.id, + details: { requestId, action: resolution.action }, + }); + res.status(202).json({ accepted: true, commandId: queued.commandId }); + } catch (error) { + if (error instanceof RunnerPrpRuntimeRequestResolutionError) { + throw conflict( + error.code === "runtime_request_resolution_conflict" + ? "A different response was already submitted for this runtime request." + : "The runner session is no longer accepting runtime responses.", + ); + } + throw error; } - throw error; - } - }); + }, + ); router.post("/heartbeat-runs/:runId/watchdog-decisions", async (req, res) => { const runId = req.params.runId as string; - const existing = await getAccessibleResource(req, res, heartbeat.getRun(runId), "Heartbeat run not found"); + const existing = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); if (!existing) return; - const decision = typeof req.body?.decision === "string" ? req.body.decision : ""; - if (!["snooze", "continue", "dismissed_false_positive"].includes(decision)) { + const decision = + typeof req.body?.decision === "string" ? req.body.decision : ""; + if ( + !["snooze", "continue", "dismissed_false_positive"].includes(decision) + ) { res.status(400).json({ error: "Unsupported watchdog decision" }); return; } - const evaluationIssueId = typeof req.body?.evaluationIssueId === "string" ? req.body.evaluationIssueId : null; - const reason = typeof req.body?.reason === "string" ? req.body.reason.slice(0, 4000) : null; - const snoozedUntil = decision === "snooze" - ? new Date(String(req.body?.snoozedUntil ?? "")) - : null; - if (decision === "snooze" && (!snoozedUntil || Number.isNaN(snoozedUntil.getTime()) || snoozedUntil <= new Date())) { - res.status(400).json({ error: "snoozedUntil must be a future ISO datetime" }); + const evaluationIssueId = + typeof req.body?.evaluationIssueId === "string" + ? req.body.evaluationIssueId + : null; + const reason = + typeof req.body?.reason === "string" + ? req.body.reason.slice(0, 4000) + : null; + const snoozedUntil = + decision === "snooze" + ? new Date(String(req.body?.snoozedUntil ?? "")) + : null; + if ( + decision === "snooze" && + (!snoozedUntil || + Number.isNaN(snoozedUntil.getTime()) || + snoozedUntil <= new Date()) + ) { + res + .status(400) + .json({ error: "snoozedUntil must be a future ISO datetime" }); return; } @@ -5121,27 +6266,173 @@ export function agentRoutes( res.json(row); }); + router.get("/heartbeat-runs/:runId/provider-trace", async (req, res) => { + assertInstanceAdmin(req); + const runId = req.params.runId as string; + const run = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); + if (!run) return; + const inspection = await providerTraces.inspect(run.id, run.companyId); + await logActivity(db, { + companyId: run.companyId, + actorType: "user", + actorId: req.actor.userId ?? "local-admin", + action: "provider_trace.redacted_viewed", + entityType: "heartbeat_run", + entityId: run.id, + details: { + traceId: inspection.trace?.id ?? null, + rawPayloadRevealed: false, + }, + }); + res.set("Cache-Control", "no-cache, no-store"); + res.json(inspection); + }); + + router.post( + "/heartbeat-runs/:runId/provider-trace/frames/:frameId/reveal", + async (req, res) => { + assertInstanceAdmin(req); + const runId = req.params.runId as string; + const frameId = Number(req.params.frameId); + if (!Number.isSafeInteger(frameId) || frameId < 1) + throw badRequest("Invalid provider trace frame id"); + const run = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); + if (!run) return; + const frame = await providerTraces.revealFrame( + run.id, + run.companyId, + frameId, + ); + if (!frame) throw notFound("Provider trace frame not found"); + await logActivity(db, { + companyId: run.companyId, + actorType: "user", + actorId: req.actor.userId ?? "local-admin", + action: "provider_trace.frame_revealed", + entityType: "heartbeat_run", + entityId: run.id, + details: { + frameId, + digest: frame.digest, + byteLength: frame.byteLength, + }, + }); + res.set("Cache-Control", "no-cache, no-store"); + res.json(frame); + }, + ); + + router.get( + "/heartbeat-runs/:runId/provider-trace/download", + async (req, res) => { + assertInstanceAdmin(req); + const runId = req.params.runId as string; + const run = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); + if (!run) return; + const download = await providerTraces.download(run.id, run.companyId); + if (!download) throw notFound("Provider trace not found"); + await logActivity(db, { + companyId: run.companyId, + actorType: "user", + actorId: req.actor.userId ?? "local-admin", + action: "provider_trace.downloaded", + entityType: "heartbeat_run", + entityId: run.id, + details: { + traceId: download.row.id, + byteCount: download.bytes.byteLength, + digest: download.row.digest, + }, + }); + res.set("Cache-Control", "no-cache, no-store"); + res.set("Content-Type", "application/x-ndjson"); + res.set( + "Content-Disposition", + `attachment; filename=provider-trace-${run.id}.ndjson`, + ); + res.send(download.bytes); + }, + ); + + router.delete("/heartbeat-runs/:runId/provider-trace", async (req, res) => { + assertInstanceAdmin(req); + const runId = req.params.runId as string; + const run = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); + if (!run) return; + const removed = await providerTraces.remove(run.id, run.companyId); + if (!removed) throw notFound("Provider trace not found"); + await logActivity(db, { + companyId: run.companyId, + actorType: "user", + actorId: req.actor.userId ?? "local-admin", + action: "provider_trace.deleted", + entityType: "heartbeat_run", + entityId: run.id, + details: { traceId: removed.id, recoverable: false }, + }); + res.json({ ok: true }); + }); + router.get("/heartbeat-runs/:runId/events", async (req, res) => { const runId = req.params.runId as string; - const run = await getAccessibleResource(req, res, heartbeat.getRun(runId), "Heartbeat run not found"); + const run = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); if (!run) return; const afterSeq = Number(req.query.afterSeq ?? 0); const limit = Number(req.query.limit ?? 200); - const events = await heartbeat.listEvents(runId, Number.isFinite(afterSeq) ? afterSeq : 0, Number.isFinite(limit) ? limit : 200); + const events = await heartbeat.listEvents( + runId, + Number.isFinite(afterSeq) ? afterSeq : 0, + Number.isFinite(limit) ? limit : 200, + ); const currentUserRedactionOptions = await getCurrentUserRedactionOptions(); const redactedEvents = events.map((event) => - redactCurrentUserValue({ - ...event, - payload: redactEventPayload(event.payload), - }, currentUserRedactionOptions), + redactCurrentUserValue( + { + ...event, + payload: redactEventPayload(event.payload), + }, + currentUserRedactionOptions, + ), + ); + res.json( + await runRedactions.redactForRun(run.companyId, run.id, redactedEvents), ); - res.json(await runRedactions.redactForRun(run.companyId, run.id, redactedEvents)); }); router.get("/heartbeat-runs/:runId/log", async (req, res) => { const runId = req.params.runId as string; - const run = await getAccessibleResource(req, res, heartbeat.getRunLogAccess(runId), "Heartbeat run not found"); + const run = await getAccessibleResource( + req, + res, + heartbeat.getRunLogAccess(runId), + "Heartbeat run not found", + ); if (!run) return; const offset = Number(req.query.offset ?? 0); @@ -5155,20 +6446,43 @@ export function agentRoutes( res.json(await runRedactions.redactForRun(run.companyId, run.id, result)); }); - router.get("/heartbeat-runs/:runId/workspace-operations", async (req, res) => { - const runId = req.params.runId as string; - const run = await getAccessibleResource(req, res, heartbeat.getRun(runId), "Heartbeat run not found"); - if (!run) return; + router.get( + "/heartbeat-runs/:runId/workspace-operations", + async (req, res) => { + const runId = req.params.runId as string; + const run = await getAccessibleResource( + req, + res, + heartbeat.getRun(runId), + "Heartbeat run not found", + ); + if (!run) return; - const context = asRecord(run.contextSnapshot); - const executionWorkspaceId = asNonEmptyString(context?.executionWorkspaceId); - const operations = await workspaceOperations.listForRun(runId, executionWorkspaceId); - res.json(redactCurrentUserValue(operations, await getCurrentUserRedactionOptions())); - }); + const context = asRecord(run.contextSnapshot); + const executionWorkspaceId = asNonEmptyString( + context?.executionWorkspaceId, + ); + const operations = await workspaceOperations.listForRun( + runId, + executionWorkspaceId, + ); + res.json( + redactCurrentUserValue( + operations, + await getCurrentUserRedactionOptions(), + ), + ); + }, + ); router.get("/workspace-operations/:operationId/log", async (req, res) => { const operationId = req.params.operationId as string; - const operation = await getAccessibleResource(req, res, workspaceOperations.getById(operationId), "Workspace operation not found"); + const operation = await getAccessibleResource( + req, + res, + workspaceOperations.getById(operationId), + "Workspace operation not found", + ); if (!operation) return; const offset = Number(req.query.offset ?? 0); @@ -5189,7 +6503,9 @@ export function agentRoutes( const issue = await getAccessibleResource( req, res, - identifier ? issueSvc.getByIdentifier(identifier) : issueSvc.getById(rawId), + identifier + ? issueSvc.getByIdentifier(identifier) + : issueSvc.getById(rawId), "Issue not found", ); if (!issue) return; @@ -5200,8 +6516,16 @@ export function agentRoutes( status: heartbeatRuns.status, invocationSource: heartbeatRuns.invocationSource, triggerDetail: heartbeatRuns.triggerDetail, - contextCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"), - contextWakeCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"), + contextCommentId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as( + "contextCommentId", + ), + contextWakeCommentId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as( + "contextWakeCommentId", + ), startedAt: heartbeatRuns.startedAt, finishedAt: heartbeatRuns.finishedAt, createdAt: heartbeatRuns.createdAt, @@ -5231,10 +6555,20 @@ export function agentRoutes( ) .orderBy(desc(heartbeatRuns.createdAt)); - res.json(await Promise.all(liveRuns.map(async (run) => ({ - ...heartbeat.decorateActiveRunStatus(run, { companyId: issue.companyId, issueId: issue.id }), - outputSilence: await heartbeat.buildRunOutputSilence({ ...run, companyId: issue.companyId }), - })))); + res.json( + await Promise.all( + liveRuns.map(async (run) => ({ + ...heartbeat.decorateActiveRunStatus(run, { + companyId: issue.companyId, + issueId: issue.id, + }), + outputSilence: await heartbeat.buildRunOutputSilence({ + ...run, + companyId: issue.companyId, + }), + })), + ), + ); }); router.get("/issues/:issueId/active-run", async (req, res) => { @@ -5244,24 +6578,28 @@ export function agentRoutes( const issue = await getAccessibleResource( req, res, - identifier ? issueSvc.getByIdentifier(identifier) : issueSvc.getById(rawId), + identifier + ? issueSvc.getByIdentifier(identifier) + : issueSvc.getById(rawId), "Issue not found", ); if (!issue) return; - let run = issue.executionRunId ? await heartbeat.getRunIssueSummary(issue.executionRunId) : null; + let run = issue.executionRunId + ? await heartbeat.getRunIssueSummary(issue.executionRunId) + : null; if ( run && - ( - (run.status !== "queued" && run.status !== "running") || - run.issueId !== issue.id - ) + ((run.status !== "queued" && run.status !== "running") || + run.issueId !== issue.id) ) { run = null; } if (!run && issue.assigneeAgentId && issue.status === "in_progress") { - const candidateRun = await heartbeat.getActiveRunIssueSummaryForAgent(issue.assigneeAgentId); + const candidateRun = await heartbeat.getActiveRunIssueSummaryForAgent( + issue.assigneeAgentId, + ); const candidateIssueId = asNonEmptyString(candidateRun?.issueId); if (candidateRun && candidateIssueId === issue.id) { run = candidateRun; @@ -5278,13 +6616,19 @@ export function agentRoutes( return; } - const decoratedRun = heartbeat.decorateActiveRunStatus(run, { companyId: issue.companyId, issueId: issue.id }); + const decoratedRun = heartbeat.decorateActiveRunStatus(run, { + companyId: issue.companyId, + issueId: issue.id, + }); res.json({ ...decoratedRun, agentId: agent.id, agentName: agent.name, adapterType: agent.adapterType, - outputSilence: await heartbeat.buildRunOutputSilence({ ...run, companyId: issue.companyId }), + outputSilence: await heartbeat.buildRunOutputSilence({ + ...run, + companyId: issue.companyId, + }), }); }); diff --git a/server/src/services/activity.ts b/server/src/services/activity.ts index a9c6f592dd0..26ba1d4eee0 100644 --- a/server/src/services/activity.ts +++ b/server/src/services/activity.ts @@ -402,6 +402,9 @@ export function activityService(db: Db) { continuationAttempt: heartbeatRuns.continuationAttempt, lastUsefulActionAt: heartbeatRuns.lastUsefulActionAt, nextAction: heartbeatRuns.nextAction, + wakeCommentIds: sql`${heartbeatRuns.contextSnapshot} -> 'wakeCommentIds'`, + wakeCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`, + contextCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'commentId'`, }) .from(heartbeatRuns) .innerJoin( diff --git a/server/src/services/heartbeat-run-summary.ts b/server/src/services/heartbeat-run-summary.ts index 73252a29ac4..967b1f8b211 100644 --- a/server/src/services/heartbeat-run-summary.ts +++ b/server/src/services/heartbeat-run-summary.ts @@ -1,20 +1,27 @@ +import type { + RunPresentationDecision, + RunPresentationSource, +} from "@paperclipai/shared"; + export const HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS = 500; export const HEARTBEAT_RUN_RESULT_OUTPUT_MAX_CHARS = 4_096; export const HEARTBEAT_RUN_SAFE_RESULT_JSON_MAX_BYTES = 64 * 1024; -function truncateSummaryText(value: unknown, maxLength = HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS) { +function truncateSummaryText( + value: unknown, + maxLength = HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS, +) { if (typeof value !== "string") return null; return value.length > maxLength ? value.slice(0, maxLength) : value; } function readNumericField(record: Record, key: string) { - return key in record ? record[key] ?? null : undefined; + return key in record ? (record[key] ?? null) : undefined; } function readCommentText(value: unknown) { if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; + return value.trim().length > 0 ? value : null; } export function mergeHeartbeatRunResultJson( @@ -48,7 +55,11 @@ export function mergeHeartbeatRunResultJson( export function summarizeHeartbeatRunResultJson( resultJson: Record | null | undefined, ): Record | null { - if (!resultJson || typeof resultJson !== "object" || Array.isArray(resultJson)) { + if ( + !resultJson || + typeof resultJson !== "object" || + Array.isArray(resultJson) + ) { return null; } @@ -61,7 +72,11 @@ export function summarizeHeartbeatRunResultJson( } } - const numericFieldAliases = ["total_cost_usd", "cost_usd", "costUsd"] as const; + const numericFieldAliases = [ + "total_cost_usd", + "cost_usd", + "costUsd", + ] as const; for (const key of numericFieldAliases) { const value = readNumericField(resultJson, key); if (value !== undefined && value !== null) { @@ -92,39 +107,194 @@ export function summarizeHeartbeatRunResultJson( return Object.keys(summary).length > 0 ? summary : null; } -// The fallback comment is only posted when a run ends without the agent posting -// its own comment via the API. In that case `resultJson.summary` can be raw -// inter-tool narration (assistantTexts concatenated by the adapter), which must -// never be published verbatim to the board — see BRO-1507 / BRO-1516. -export const MAX_FALLBACK_COMMENT_CHARS = 1200; +// An untyped adapter summary can be raw inter-tool narration (assistantTexts +// concatenated by the adapter), which must never be published verbatim to the +// board — see BRO-1507 / BRO-1516. Typed final messages and accepted PRP results +// are semantic output, so they intentionally bypass this legacy safety check. // Apostrophes are matched as a character class so both the straight (') and // curly (’) forms count — agents emit either. Openers are narration phrases a // declarative status summary would not begin with ("Fixed X", "13/13 pass"). const NARRATION_OPENERS = /^(let me\b|i['’]ll\b|i['’]m going\b|i need to\b|i can see\b|now i['’]ll\b|next,? i['’]ll\b|looking at\b|fetching\b|checking\b|first,)/i; -const FALLBACK_WITHHELD_COMMENT = + +export const LEGACY_WITHHELD_RUN_COMMENT = "Run completed. Agent did not post a summary comment this run (transcript withheld — see run log)."; +export const RUN_PRESENTATION_RESOLVER_VERSION = "1"; + +export type RunPresentationCommentAction = "reuse" | "create" | "none"; +export type { RunPresentationDecision } from "@paperclipai/shared"; + +export interface ResolvedHeartbeatRunResponse { + text: string | null; + decision: RunPresentationDecision; +} + +function record(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +} + +function readAcceptedSemanticSummary(resultJson: Record) { + const candidates = [ + record(resultJson.nativeResult), + record(resultJson.acceptedResult), + record(record(resultJson.semanticResult).result), + ]; + for (const candidate of candidates) { + if (candidate.schema !== "paperclip.run_result.v1") continue; + const summary = readCommentText(candidate.summary); + if (summary) return summary; + } + return null; +} + +export function projectHistoricalHeartbeatRunComment( + body: string, + resultJson: Record | null | undefined, +) { + if (body !== LEGACY_WITHHELD_RUN_COMMENT) return body; + return readAcceptedSemanticSummary(record(resultJson)) ?? body; +} + +function readMarkedAdapterFinalResponse(resultJson: Record) { + const structured = record(resultJson.finalResponse); + if (structured.disposition === "final" || structured.final === true) { + return ( + readCommentText(structured.text) ?? readCommentText(structured.message) + ); + } + if (resultJson.finalResponseDisposition === "final") { + return ( + readCommentText(resultJson.finalResponseText) ?? + readCommentText(resultJson.finalResponse) + ); + } + return null; +} + +function isStructuredSemanticResultText(value: string) { + try { + const parsed = JSON.parse(value) as unknown; + return Boolean( + parsed && + typeof parsed === "object" && + !Array.isArray(parsed) && + (parsed as Record).schema === "paperclip.run_result.v1", + ); + } catch { + return false; + } +} + +function decision( + chosenSource: RunPresentationSource, + input: { + sourceEventId?: string | null; + commentAction: RunPresentationCommentAction; + commentId?: string | null; + reasonCodes: string[]; + }, +): RunPresentationDecision { + return { + schema: "paperclip.run_presentation_decision.v1", + resolverVersion: RUN_PRESENTATION_RESOLVER_VERSION, + chosenSource, + sourceEventId: input.sourceEventId ?? null, + commentAction: input.commentAction, + commentId: input.commentId ?? null, + activityDisposition: "collapse", + reasonCodes: input.reasonCodes, + }; +} + +/** + * Resolve durable user-facing prose independently from the semantic run status. + * The returned text is never truncated. Callers may persist only the bounded + * decision record and materialize the exact text as an issue comment. + */ +export function resolveHeartbeatRunResponse(input: { + resultJson: Record | null | undefined; + existingComment?: { id: string; body?: string | null } | null; + finalAgentMessage?: { text: string; sourceEventId: string | null } | null; +}): ResolvedHeartbeatRunResponse { + const existingText = readCommentText(input.existingComment?.body); + if (input.existingComment && existingText) { + return { + text: existingText, + decision: decision("existing_issue_comment", { + commentAction: "reuse", + commentId: input.existingComment.id, + reasonCodes: ["explicit_non_progress_comment_precedence"], + }), + }; + } + + const finalAgentText = readCommentText(input.finalAgentMessage?.text); + if (finalAgentText && !isStructuredSemanticResultText(finalAgentText)) { + return { + text: finalAgentText, + decision: decision("final_agent_message", { + sourceEventId: input.finalAgentMessage?.sourceEventId, + commentAction: "create", + reasonCodes: ["latest_non_empty_completed_final_agent_message"], + }), + }; + } + + const resultJson = record(input.resultJson); + const semanticSummary = readAcceptedSemanticSummary(resultJson); + if (semanticSummary) { + return { + text: semanticSummary, + decision: decision("semantic_result_summary", { + commentAction: "create", + reasonCodes: ["accepted_semantic_result_summary"], + }), + }; + } + + const adapterFinal = readMarkedAdapterFinalResponse(resultJson); + if (adapterFinal) { + return { + text: adapterFinal, + decision: decision("adapter_final_response", { + commentAction: "create", + reasonCodes: ["adapter_output_marked_final"], + }), + }; + } + + const legacyText = + readCommentText(resultJson.summary) ?? + readCommentText(resultJson.result) ?? + readCommentText(resultJson.message); + if (legacyText && !NARRATION_OPENERS.test(legacyText.trimStart())) { + return { + text: legacyText, + decision: decision("adapter_final_response", { + commentAction: "create", + reasonCodes: ["legacy_adapter_summary_compatibility"], + }), + }; + } + + return { + text: null, + decision: decision("none", { + commentAction: "none", + reasonCodes: legacyText + ? ["legacy_adapter_summary_ambiguous"] + : ["no_user_facing_response"], + }), + }; +} + export function buildHeartbeatRunIssueComment( resultJson: Record | null | undefined, ): string | null { - if (!resultJson || typeof resultJson !== "object" || Array.isArray(resultJson)) { - return null; - } - - const text = - readCommentText(resultJson.summary) - ?? readCommentText(resultJson.result) - ?? readCommentText(resultJson.message); - if (!text) { - return null; - } - - if (text.length > MAX_FALLBACK_COMMENT_CHARS || NARRATION_OPENERS.test(text)) { - return FALLBACK_WITHHELD_COMMENT; - } - - return text; + return resolveHeartbeatRunResponse({ resultJson }).text; } export function findHeartbeatRunCompletionComment( @@ -138,16 +308,20 @@ export function findHeartbeatRunCompletionComment( const progressCommentIds = new Set(); for (const receipt of Object.values(receipts)) { - if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) continue; + if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) + continue; const receiptRecord = receipt as Record; if (receiptRecord.operationId !== "report_progress") continue; const result = receiptRecord.result; - if (!result || typeof result !== "object" || Array.isArray(result)) continue; + if (!result || typeof result !== "object" || Array.isArray(result)) + continue; const commentId = (result as Record).commentId; if (typeof commentId === "string" && commentId.length > 0) { progressCommentIds.add(commentId); } } - return comments.find((comment) => !progressCommentIds.has(comment.id)) ?? null; + return ( + comments.find((comment) => !progressCommentIds.has(comment.id)) ?? null + ); } diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 6e68892acbd..7c394134618 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -3,7 +3,23 @@ import path from "node:path"; import { execFile as execFileCallback } from "node:child_process"; import { promisify } from "node:util"; import { createHash, randomUUID } from "node:crypto"; -import { and, asc, desc, eq, getTableColumns, gt, gte, inArray, isNull, lt, lte, ne, notInArray, or, sql } from "drizzle-orm"; +import { + and, + asc, + desc, + eq, + getTableColumns, + gt, + gte, + inArray, + isNull, + lt, + lte, + ne, + notInArray, + or, + sql, +} from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { AGENT_DEFAULT_MAX_CONCURRENT_RUNS, @@ -106,7 +122,15 @@ import { } from "../vendor/paperclip-runner/index.js"; import { normalizeResponsibleUserDenialCode } from "./responsible-user-denial-run-outcomes.js"; import { getRunLogStore, type RunLogHandle } from "./run-log-store.js"; -import { getServerAdapter, listAdapterModelProfiles, runningProcesses } from "../adapters/index.js"; +import { + providerTraceStore, + PROVIDER_TRACE_MAX_BYTES, +} from "./provider-trace-store.js"; +import { + getServerAdapter, + listAdapterModelProfiles, + runningProcesses, +} from "../adapters/index.js"; import type { AdapterExecutionResult, AdapterInvocationMeta, @@ -118,14 +142,23 @@ import type { UsageSummary, } from "../adapters/index.js"; import { createLocalAgentJwt } from "../agent-auth-jwt.js"; -import { parseObject, asBoolean, asNumber, appendWithByteCap, MAX_EXCERPT_BYTES } from "../adapters/utils.js"; +import { + parseObject, + asBoolean, + asNumber, + appendWithByteCap, + MAX_EXCERPT_BYTES, +} from "../adapters/utils.js"; import { costService } from "./costs.js"; import { trackAgentFirstHeartbeat } from "@paperclipai/shared/telemetry"; import { getTelemetryClient } from "../telemetry.js"; import { companySkillService } from "./company-skills.js"; import { budgetService, type BudgetEnforcementScope } from "./budgets.js"; import { secretService, type MissingRuntimeBinding } from "./secrets.js"; -import { resolveDefaultAgentWorkspaceDir, resolveManagedProjectWorkspaceDir } from "../home-paths.js"; +import { + resolveDefaultAgentWorkspaceDir, + resolveManagedProjectWorkspaceDir, +} from "../home-paths.js"; import { buildHeartbeatRunIssueComment, findHeartbeatRunCompletionComment, @@ -133,6 +166,8 @@ import { HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS, HEARTBEAT_RUN_SAFE_RESULT_JSON_MAX_BYTES, mergeHeartbeatRunResultJson, + resolveHeartbeatRunResponse, + type RunPresentationDecision, } from "./heartbeat-run-summary.js"; import { buildHeartbeatRunStopMetadata, @@ -151,7 +186,11 @@ import { evaluateIssueRewakeThrottle, isThrottleCandidateIssueRewake, } from "./issue-rewake-throttle.js"; -import { logActivity, publishPluginDomainEvent, type LogActivityInput } from "./activity-log.js"; +import { + logActivity, + publishPluginDomainEvent, + type LogActivityInput, +} from "./activity-log.js"; import { buildWorkspaceReadyComment, buildWorkspaceReadyMetadata, @@ -176,7 +215,10 @@ import { } from "./workspace-instance-cleanup.js"; import { issueService } from "./issues.js"; import { projectService } from "./projects.js"; -import { authorizationService, type AuthorizationActor } from "./authorization.js"; +import { + authorizationService, + type AuthorizationActor, +} from "./authorization.js"; import { createToolGatewayService } from "./tool-gateway.js"; import { toolAccessService } from "./tool-access.js"; import { visibleIssueCondition } from "./issue-visibility.js"; @@ -197,10 +239,22 @@ import { getIssueContinuationSummaryDocument, refreshIssueContinuationSummary, } from "./issue-continuation-summary.js"; -import { buildDocumentReviewContext, buildPlanReviewContext } from "./plan-review-context.js"; -import { executionWorkspaceService, mergeExecutionWorkspaceConfig } from "./execution-workspaces.js"; -import { workspaceOperationService, type WorkspaceOperationRecorder } from "./workspace-operations.js"; -import { isProcessGroupAlive, terminateLocalService } from "./local-service-supervisor.js"; +import { + buildDocumentReviewContext, + buildPlanReviewContext, +} from "./plan-review-context.js"; +import { + executionWorkspaceService, + mergeExecutionWorkspaceConfig, +} from "./execution-workspaces.js"; +import { + workspaceOperationService, + type WorkspaceOperationRecorder, +} from "./workspace-operations.js"; +import { + isProcessGroupAlive, + terminateLocalService, +} from "./local-service-supervisor.js"; import { HEARTBEAT_RUN_SCRATCH_MARKER, buildHeartbeatRunScratchEnv, @@ -259,7 +313,10 @@ import { recoveryAssigneeAdapterOverrides, withRecoveryModelProfileHint, } from "./recovery/model-profile-hint.js"; -import { ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS as RECOVERY_ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS, recoveryService } from "./recovery/service.js"; +import { + ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS as RECOVERY_ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS, + recoveryService, +} from "./recovery/service.js"; import { buildIssueReviewPathLostIdempotencyKey, decideIssueReviewPathRecovery, @@ -335,7 +392,10 @@ import { assertLowTrustRuntimeServicesAllowed, assertLowTrustWorkspaceIsolation, } from "./low-trust-runtime-containment.js"; -import { resolveCoreTrustPreset, type TrustPresetResolution } from "./trust-preset-resolver.js"; +import { + resolveCoreTrustPreset, + type TrustPresetResolution, +} from "./trust-preset-resolver.js"; import { createEffectiveRunConfigFingerprints, createEffectiveRunConfigSubcategoryFingerprints, @@ -356,7 +416,9 @@ export function redactDetectedSuccessfulRunProgressSummaryForBoard( currentUserRedactionOptions?: CurrentUserRedactionOptions, ) { const normalized = summary.replace(/\s+/g, " ").trim(); - const redacted = redactSensitiveText(redactCurrentUserText(normalized, currentUserRedactionOptions)); + const redacted = redactSensitiveText( + redactCurrentUserText(normalized, currentUserRedactionOptions), + ); return redacted.length <= 280 ? redacted : `${redacted.slice(0, 277)}...`; } @@ -365,7 +427,9 @@ export function redactSuccessfulRunHandoffEvidence( currentUserRedactionOptions?: CurrentUserRedactionOptions, ) { if (!value) return null; - return redactSensitiveText(redactCurrentUserText(value, currentUserRedactionOptions)); + return redactSensitiveText( + redactCurrentUserText(value, currentUserRedactionOptions), + ); } const MAX_RUN_EVENT_PAYLOAD_OBJECT_KEYS = 100; @@ -452,10 +516,28 @@ const MAX_INLINE_WAKE_COMMENT_BODY_TOTAL_CHARS = 12_000; const MAX_INLINE_WAKE_ISSUE_DESCRIPTION_CHARS = 12_000; const MAX_AGENT_SESSION_MESSAGE_CHARS = 12_000; const execFile = promisify(execFileCallback); -const EXECUTION_PATH_HEARTBEAT_RUN_STATUSES = ["queued", "running", "scheduled_retry"] as const; -const CANCELLABLE_HEARTBEAT_RUN_STATUSES = ["queued", "running", "scheduled_retry"] as const; -const HEARTBEAT_RUN_TERMINAL_STATUSES = ["succeeded", "interrupted", "failed", "cancelled", "timed_out"] as const; -const UNSUCCESSFUL_HEARTBEAT_RUN_TERMINAL_STATUSES = ["failed", "cancelled", "timed_out"] as const; +const EXECUTION_PATH_HEARTBEAT_RUN_STATUSES = [ + "queued", + "running", + "scheduled_retry", +] as const; +const CANCELLABLE_HEARTBEAT_RUN_STATUSES = [ + "queued", + "running", + "scheduled_retry", +] as const; +const HEARTBEAT_RUN_TERMINAL_STATUSES = [ + "succeeded", + "interrupted", + "failed", + "cancelled", + "timed_out", +] as const; +const UNSUCCESSFUL_HEARTBEAT_RUN_TERMINAL_STATUSES = [ + "failed", + "cancelled", + "timed_out", +] as const; const TIMER_ACTIONABLE_ISSUE_STATUSES = ["todo", "in_progress"] as const; export { ACTIVE_RUN_OUTPUT_CONTINUE_REARM_MS, @@ -473,19 +555,30 @@ export const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_DELAYS_MS = [ const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_JITTER_RATIO = 0.25; const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_REASON = "transient_failure"; const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_WAKE_REASON = "transient_failure_retry"; -const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS = BOUNDED_TRANSIENT_HEARTBEAT_RETRY_DELAYS_MS.length; -export const INTERACTION_CONTINUATION_INFRA_RETRY_REASON = "interaction_continuation_infra_retry"; -export const INTERACTION_CONTINUATION_INFRA_WAKE_REASON = "interaction_continuation_infra_retry"; +const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS = + BOUNDED_TRANSIENT_HEARTBEAT_RETRY_DELAYS_MS.length; +export const INTERACTION_CONTINUATION_INFRA_RETRY_REASON = + "interaction_continuation_infra_retry"; +export const INTERACTION_CONTINUATION_INFRA_WAKE_REASON = + "interaction_continuation_infra_retry"; const INTERACTION_CONTINUATION_INFRA_MAX_ATTEMPTS = 3; -const RESOLVED_INTERACTION_CONTINUATION_STATUSES = new Set(["accepted", "answered", "rejected"]); +const RESOLVED_INTERACTION_CONTINUATION_STATUSES = new Set([ + "accepted", + "answered", + "rejected", +]); const WORKSPACE_VALIDATION_FAILURE_CODE = "workspace_validation_failed"; const WORKSPACE_VALIDATION_RECOVERY_CAUSE = "workspace_validation_failed"; const CONFIGURATION_INCOMPLETE_FAILURE_CODE = "configuration_incomplete"; const CONFIGURATION_INCOMPLETE_RECOVERY_CAUSE = "configuration_incomplete"; -const EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON = "execution_review_participant_recovery"; -const EXECUTION_REVIEW_PARTICIPANT_RECOVERY_WAKE_REASON = "execution_review_participant_recovery"; -const EXECUTION_REVIEW_PARTICIPANT_RECOVERY_CAUSE = "execution_review_participant_recovery"; -const GITHUB_PR_WORKFLOW_SKILL_KEY = "paperclipai/bundled/software-development/github-pr-workflow"; +const EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON = + "execution_review_participant_recovery"; +const EXECUTION_REVIEW_PARTICIPANT_RECOVERY_WAKE_REASON = + "execution_review_participant_recovery"; +const EXECUTION_REVIEW_PARTICIPANT_RECOVERY_CAUSE = + "execution_review_participant_recovery"; +const GITHUB_PR_WORKFLOW_SKILL_KEY = + "paperclipai/bundled/software-development/github-pr-workflow"; const GITHUB_PR_WORKFLOW_SKILL_SLUG = "github-pr-workflow"; const PUSH_CAPABILITY_ENV_KEYS = ["GH_TOKEN", "GITHUB_TOKEN"] as const; // Keep this in sync with local adapters that require a git workspace before launch. @@ -505,7 +598,11 @@ const MAX_TURN_CONTINUATION_DEFAULT_MAX_ATTEMPTS = 2; const MAX_TURN_CONTINUATION_MAX_ATTEMPTS_CAP = 10; const MAX_TURN_CONTINUATION_DEFAULT_DELAY_MS = 1_000; const MAX_TURN_CONTINUATION_MAX_DELAY_MS = 5 * 60 * 1000; -const MAX_TURN_CONTINUATION_LIVE_RUN_STATUSES = ["scheduled_retry", "queued", "running"] as const; +const MAX_TURN_CONTINUATION_LIVE_RUN_STATUSES = [ + "scheduled_retry", + "queued", + "running", +] as const; export const WORKSPACE_BUSY_RETRY_REASON = "workspace_busy"; export const WORKSPACE_BUSY_RETRY_WAKE_REASON = "workspace_busy_retry"; export const WORKSPACE_BUSY_ERROR_CODE = "workspace_busy"; @@ -519,14 +616,19 @@ export const WORKSPACE_BUSY_RETRY_JITTER_MS = 60 * 1000; // contrast, never gets overtaken — a deferred run keeps rescheduling until the // workspace frees, because dispatching alongside a live holder is exactly the // concurrent-mutation failure this gate exists to prevent. -export const WORKSPACE_BUSY_HOLDER_STALE_AFTER_MS = RECOVERY_ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS; +export const WORKSPACE_BUSY_HOLDER_STALE_AFTER_MS = + RECOVERY_ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS; // Issue-level executionWorkspaceSettings.mode values that unambiguously opt an // issue's runs out of the shared project workspace, and therefore out of // shared-workspace serialization ("isolated" is the legacy alias // parseIssueExecutionWorkspaceSettings normalizes to isolated_workspace). Any // other value — including agent_default and an absent mode — may still resolve // to the shared workspace and counts as a holder. -const ISOLATED_EXECUTION_WORKSPACE_MODES = ["isolated_workspace", "operator_branch", "isolated"] as const; +const ISOLATED_EXECUTION_WORKSPACE_MODES = [ + "isolated_workspace", + "operator_branch", + "isolated", +] as const; type CodexTransientFallbackMode = | "same_session" | "safer_invocation" @@ -601,13 +703,20 @@ export class WorkspaceBusyDeferral extends Error { } } -function isWorkspaceBusyDeferral(error: unknown): error is WorkspaceBusyDeferral { +function isWorkspaceBusyDeferral( + error: unknown, +): error is WorkspaceBusyDeferral { return error instanceof WorkspaceBusyDeferral; } -export function computeWorkspaceBusyRetryDelayMs(random: () => number = Math.random) { +export function computeWorkspaceBusyRetryDelayMs( + random: () => number = Math.random, +) { const jitter = Math.min(Math.max(random(), 0), 1); - return WORKSPACE_BUSY_RETRY_BASE_DELAY_MS + Math.floor(jitter * WORKSPACE_BUSY_RETRY_JITTER_MS); + return ( + WORKSPACE_BUSY_RETRY_BASE_DELAY_MS + + Math.floor(jitter * WORKSPACE_BUSY_RETRY_JITTER_MS) + ); } // True for the retry of a workspace-busy deferral whose original run did NOT @@ -626,7 +735,9 @@ export function isNonAssigneeWorkspaceBusyRetry( ); } -function resolveCodexTransientFallbackMode(attempt: number): CodexTransientFallbackMode { +function resolveCodexTransientFallbackMode( + attempt: number, +): CodexTransientFallbackMode { if (attempt <= 1) return "same_session"; if (attempt === 2) return "safer_invocation"; if (attempt === 3) return "fresh_session"; @@ -659,14 +770,20 @@ function isMaxTurnExhaustionRun( const resultJson = parseObject(run.resultJson); return Boolean( normalizeMaxTurnStopReason(resultJson.stopReason) ?? - normalizeMaxTurnStopReason(run.errorCode), + normalizeMaxTurnStopReason(run.errorCode), ); } -function readTransientRetryNotBeforeFromRun(run: Pick) { +function readTransientRetryNotBeforeFromRun( + run: Pick, +) { const resultJson = parseObject(run.resultJson); const value = resultJson.retryNotBefore ?? resultJson.transientRetryNotBefore; - if (!(typeof value === "string" || typeof value === "number" || value instanceof Date)) { + if (!( + typeof value === "string" || + typeof value === "number" || + value instanceof Date + )) { return null; } const parsed = new Date(value); @@ -677,7 +794,8 @@ function readTransientRecoveryContractFromRun( run: Pick, ) { const errorFamily = readHeartbeatRunErrorFamily(run); - return errorFamily === "transient_upstream" || errorFamily === "provider_quota" + return errorFamily === "transient_upstream" || + errorFamily === "provider_quota" ? { errorFamily, retryNotBefore: readTransientRetryNotBeforeFromRun(run), @@ -685,12 +803,15 @@ function readTransientRecoveryContractFromRun( : null; } -function isResolvedInteractionContinuationWakeContext(contextSnapshot: unknown) { +function isResolvedInteractionContinuationWakeContext( + contextSnapshot: unknown, +) { const context = parseObject(contextSnapshot); const interactionId = readNonEmptyString(context.interactionId); const interactionStatus = readNonEmptyString(context.interactionStatus); if (!interactionId || !interactionStatus) return false; - if (!RESOLVED_INTERACTION_CONTINUATION_STATUSES.has(interactionStatus)) return false; + if (!RESOLVED_INTERACTION_CONTINUATION_STATUSES.has(interactionStatus)) + return false; const mutation = readNonEmptyString(context.mutation); const wakeReason = readNonEmptyString(context.wakeReason); @@ -725,17 +846,26 @@ function isSpawnLikeFailureMessage(value: unknown) { // describes a terminal condition that must not be retried. function isSandboxProviderWorkerUnavailableFailureMessage(value: unknown) { if (typeof value !== "string") return false; - return /sandbox provider .* is installed via plugin .* but its worker is not running/i.test(value); + return /sandbox provider .* is installed via plugin .* but its worker is not running/i.test( + value, + ); } function isRetryableInteractionContinuationInfrastructureFailure( - run: Pick, + run: Pick< + typeof heartbeatRuns.$inferSelect, + "error" | "errorCode" | "resultJson" + >, ) { - if (run.errorCode === WORKSPACE_VALIDATION_FAILURE_CODE || run.errorCode === "process_lost") { + if ( + run.errorCode === WORKSPACE_VALIDATION_FAILURE_CODE || + run.errorCode === "process_lost" + ) { return true; } - if (run.errorCode !== "adapter_failed" && run.errorCode !== "setup_failed") return false; + if (run.errorCode !== "adapter_failed" && run.errorCode !== "setup_failed") + return false; const resultJson = parseObject(run.resultJson); return ( @@ -755,7 +885,8 @@ function mergeAdapterRecoveryMetadata(input: { }) { const errorFamily = readNonEmptyString(input.errorFamily); const retryNotBefore = readNonEmptyString(input.retryNotBefore); - if (!input.resultJson && !errorFamily && !retryNotBefore) return input.resultJson ?? null; + if (!input.resultJson && !errorFamily && !retryNotBefore) + return input.resultJson ?? null; return { ...(input.resultJson ?? {}), @@ -764,7 +895,9 @@ function mergeAdapterRecoveryMetadata(input: { ? { retryNotBefore, transientRetryNotBefore: retryNotBefore, - ...(errorFamily === "provider_quota" ? { providerQuotaRetryNotBefore: retryNotBefore } : {}), + ...(errorFamily === "provider_quota" + ? { providerQuotaRetryNotBefore: retryNotBefore } + : {}), } : {}), }; @@ -826,7 +959,8 @@ class NativeSessionResumeScheduledError extends Error { this.name = "NativeSessionResumeScheduledError"; } } -const INLINE_BASE64_IMAGE_DATA_RE = /("type":"image","source":\{"type":"base64","data":")([A-Za-z0-9+/=]{1024,})(")/g; +const INLINE_BASE64_IMAGE_DATA_RE = + /("type":"image","source":\{"type":"base64","data":")([A-Za-z0-9+/=]{1024,})(")/g; type RuntimeConfigSecretResolver = Pick< ReturnType, | "resolveAdapterConfigForRuntime" @@ -835,20 +969,23 @@ type RuntimeConfigSecretResolver = Pick< | "collectMissingAdapterConfigRuntimeBindings" >; -function formatMissingBindingForOperator(missing: MissingRuntimeBinding): string { +function formatMissingBindingForOperator( + missing: MissingRuntimeBinding, +): string { if (missing.bindingType === "user_secret_ref") { - const definitionLabel = - missing.userSecretDefinitionName - ? `"${missing.userSecretDefinitionName}"` - : missing.userSecretDefinitionKey - ? `"${missing.userSecretDefinitionKey}"` - : "declared user secret"; - const ownerLabel = missing.responsibleUserId ? ` for responsible user ${missing.responsibleUserId}` : ""; + const definitionLabel = missing.userSecretDefinitionName + ? `"${missing.userSecretDefinitionName}"` + : missing.userSecretDefinitionKey + ? `"${missing.userSecretDefinitionKey}"` + : "declared user secret"; + const ownerLabel = missing.responsibleUserId + ? ` for responsible user ${missing.responsibleUserId}` + : ""; return `user secret ${definitionLabel}${ownerLabel} not available at ${missing.consumerType} ${missing.configPath}`; } const secretLabel = missing.secretName ? `"${missing.secretName}"` - : missing.secretId ?? "unknown"; + : (missing.secretId ?? "unknown"); return `secret ${secretLabel} not bound at ${missing.consumerType} ${missing.configPath}`; } @@ -864,9 +1001,11 @@ function isConfiguredEnvBindingValue(binding: unknown) { function hasGithubPrWorkflowSkill(desiredSkills: string[]) { return desiredSkills.some((skill) => { const normalized = skill.trim(); - return normalized === GITHUB_PR_WORKFLOW_SKILL_KEY - || normalized === GITHUB_PR_WORKFLOW_SKILL_SLUG - || normalized.endsWith(`/${GITHUB_PR_WORKFLOW_SKILL_SLUG}`); + return ( + normalized === GITHUB_PR_WORKFLOW_SKILL_KEY || + normalized === GITHUB_PR_WORKFLOW_SKILL_SLUG || + normalized.endsWith(`/${GITHUB_PR_WORKFLOW_SKILL_SLUG}`) + ); }); } @@ -875,9 +1014,11 @@ export function requiresPushCapabilityPreflight(input: { issueId: string | null | undefined; explicitRunScopedSkillKeys: string[]; }) { - return Boolean(input.issueId) - && GIT_SENSITIVE_LOCAL_ADAPTER_TYPES.has(input.adapterType) - && hasGithubPrWorkflowSkill(input.explicitRunScopedSkillKeys); + return ( + Boolean(input.issueId) && + GIT_SENSITIVE_LOCAL_ADAPTER_TYPES.has(input.adapterType) && + hasGithubPrWorkflowSkill(input.explicitRunScopedSkillKeys) + ); } const LOW_TRUST_SENSITIVE_ENV_KEY_RE = @@ -893,15 +1034,21 @@ const LOW_TRUST_SENSITIVE_ENV_KEY_RE = // the run env like any non-prefixed binding. const FORBIDDEN_ENV_BINDING_KEYS = new Set(["PAPERCLIP_API_KEY"]); -function stripForbiddenEnvBindings(envValue: unknown): Record | null { +function stripForbiddenEnvBindings( + envValue: unknown, +): Record | null { const record = parseObject(envValue); const filtered = Object.fromEntries( - Object.entries(record).filter(([key]) => !FORBIDDEN_ENV_BINDING_KEYS.has(key)), + Object.entries(record).filter( + ([key]) => !FORBIDDEN_ENV_BINDING_KEYS.has(key), + ), ); return Object.keys(filtered).length > 0 ? filtered : null; } -function stripForbiddenEnvFromAdapterConfig(config: Record): Record { +function stripForbiddenEnvFromAdapterConfig( + config: Record, +): Record { if (!Object.prototype.hasOwnProperty.call(config, "env")) return config; return { ...config, @@ -918,11 +1065,17 @@ function assertLowTrustEnvConfigAllowed(envValue: unknown, source: string) { const binding = parsed.data; const isPlainBinding = typeof binding === "string" || - (typeof binding === "object" && binding !== null && binding.type === "plain"); + (typeof binding === "object" && + binding !== null && + binding.type === "plain"); if (isPlainBinding && LOW_TRUST_SENSITIVE_ENV_KEY_RE.test(key)) { - throw new HttpError(422, `Low-trust execution cannot use inline sensitive env value ${source}.${key}`, { - code: "low_trust_inline_sensitive_env_denied", - }); + throw new HttpError( + 422, + `Low-trust execution cannot use inline sensitive env value ${source}.${key}`, + { + code: "low_trust_inline_sensitive_env_denied", + }, + ); } } } @@ -951,14 +1104,17 @@ export async function resolveExecutionRunAdapterConfig(input: { remediation: string; }; }) { - const executionRunConfig = stripForbiddenEnvFromAdapterConfig(input.executionRunConfig); + const executionRunConfig = stripForbiddenEnvFromAdapterConfig( + input.executionRunConfig, + ); const environmentEnv = stripForbiddenEnvBindings(input.environmentEnv); const projectEnv = stripForbiddenEnvBindings(input.projectEnv); const routineEnv = stripForbiddenEnvBindings(input.routineEnv); const agentEnv = parseObject(executionRunConfig.env); - const lowTrustAllowedBindingIds = input.trustPreset?.kind === "low_trust_review" - ? input.trustPreset.boundary.allowedSecretBindingIds ?? [] - : undefined; + const lowTrustAllowedBindingIds = + input.trustPreset?.kind === "low_trust_review" + ? (input.trustPreset.boundary.allowedSecretBindingIds ?? []) + : undefined; if (input.trustPreset?.kind === "low_trust_review") { assertLowTrustEnvConfigAllowed(environmentEnv, "environment.env"); assertLowTrustEnvConfigAllowed(executionRunConfig.env, "agent.env"); @@ -967,28 +1123,31 @@ export async function resolveExecutionRunAdapterConfig(input: { } const requiredScopedEnvBinding = input.requiredScopedEnvBinding ?? null; const requiredScopedBindingsConfigured = requiredScopedEnvBinding - ? requiredScopedEnvBinding.keys.some((key) => ( - requiredScopedEnvBinding.consumerScopes.includes("agent") - && isConfiguredEnvBindingValue(agentEnv[key]) - ) || ( - requiredScopedEnvBinding.consumerScopes.includes("project") - && isConfiguredEnvBindingValue(projectEnv?.[key]) - )) + ? requiredScopedEnvBinding.keys.some( + (key) => + (requiredScopedEnvBinding.consumerScopes.includes("agent") && + isConfiguredEnvBindingValue(agentEnv[key])) || + (requiredScopedEnvBinding.consumerScopes.includes("project") && + isConfiguredEnvBindingValue(projectEnv?.[key])), + ) : false; if (requiredScopedEnvBinding && !requiredScopedBindingsConfigured) { - throw new ConfigurationIncompleteFailure(`configuration incomplete: ${requiredScopedEnvBinding.remediation}`, { - configurationIncomplete: { - reason: requiredScopedEnvBinding.reason, - companyId: input.companyId, - agentId: input.agentId ?? null, - issueId: input.issueId ?? null, - projectId: input.projectId ?? null, - routineId: input.routineId ?? null, - requiredEnvKeys: requiredScopedEnvBinding.keys, - requiredScopes: requiredScopedEnvBinding.consumerScopes, - missingBindings: [], + throw new ConfigurationIncompleteFailure( + `configuration incomplete: ${requiredScopedEnvBinding.remediation}`, + { + configurationIncomplete: { + reason: requiredScopedEnvBinding.reason, + companyId: input.companyId, + agentId: input.agentId ?? null, + issueId: input.issueId ?? null, + projectId: input.projectId ?? null, + routineId: input.routineId ?? null, + requiredEnvKeys: requiredScopedEnvBinding.keys, + requiredScopes: requiredScopedEnvBinding.consumerScopes, + missingBindings: [], + }, }, - }); + ); } // Pre-dispatch binding-validation gate: detect declared secret refs that have // no binding before resolving any secret value. Missing bindings short-circuit @@ -1021,7 +1180,10 @@ export async function resolveExecutionRunAdapterConfig(input: { }, )), ); - if (typeof input.secretsSvc.collectMissingAdapterConfigRuntimeBindings === "function") { + if ( + typeof input.secretsSvc.collectMissingAdapterConfigRuntimeBindings === + "function" + ) { missingBindings.push( ...(await input.secretsSvc.collectMissingAdapterConfigRuntimeBindings( input.companyId, @@ -1065,12 +1227,15 @@ export async function resolveExecutionRunAdapterConfig(input: { if (requiredScopedEnvBinding) { const requiredEnvKeys = new Set(requiredScopedEnvBinding.keys); const requiredScopes = new Set(requiredScopedEnvBinding.consumerScopes); - const requiredMissingBindings = missingBindings.filter((binding) => - requiredScopes.has(binding.consumerType as "agent" | "project") - && requiredEnvKeys.has(binding.envKey), + const requiredMissingBindings = missingBindings.filter( + (binding) => + requiredScopes.has(binding.consumerType as "agent" | "project") && + requiredEnvKeys.has(binding.envKey), ); if (requiredMissingBindings.length > 0) { - const detail = requiredMissingBindings.map(formatMissingBindingForOperator).join("; "); + const detail = requiredMissingBindings + .map(formatMissingBindingForOperator) + .join("; "); throw new ConfigurationIncompleteFailure( `configuration incomplete: ${requiredScopedEnvBinding.remediation}; ${detail}`, { @@ -1090,18 +1255,23 @@ export async function resolveExecutionRunAdapterConfig(input: { } } if (missingBindings.length > 0) { - const detail = missingBindings.map(formatMissingBindingForOperator).join("; "); - throw new ConfigurationIncompleteFailure(`configuration incomplete: ${detail}`, { - configurationIncomplete: { - reason: "secret_binding_missing", - companyId: input.companyId, - agentId: input.agentId ?? null, - issueId: input.issueId ?? null, - projectId: input.projectId ?? null, - routineId: input.routineId ?? null, - missingBindings, + const detail = missingBindings + .map(formatMissingBindingForOperator) + .join("; "); + throw new ConfigurationIncompleteFailure( + `configuration incomplete: ${detail}`, + { + configurationIncomplete: { + reason: "secret_binding_missing", + companyId: input.companyId, + agentId: input.agentId ?? null, + issueId: input.issueId ?? null, + projectId: input.projectId ?? null, + routineId: input.routineId ?? null, + missingBindings, + }, }, - }); + ); } } const environmentEnvResolution = environmentEnv @@ -1117,12 +1287,18 @@ export async function resolveExecutionRunAdapterConfig(input: { responsibleUserId: input.responsibleUserId ?? null, issueId: input.issueId ?? null, heartbeatRunId: input.heartbeatRunId ?? null, - ...(lowTrustAllowedBindingIds !== undefined ? { allowedBindingIds: lowTrustAllowedBindingIds } : {}), + ...(lowTrustAllowedBindingIds !== undefined + ? { allowedBindingIds: lowTrustAllowedBindingIds } + : {}), } : undefined, ) : { env: {}, secretKeys: new Set(), manifest: [] }; - const { config: resolvedConfig, secretKeys, manifest } = await input.secretsSvc.resolveAdapterConfigForRuntime( + const { + config: resolvedConfig, + secretKeys, + manifest, + } = await input.secretsSvc.resolveAdapterConfigForRuntime( input.companyId, executionRunConfig, input.agentId @@ -1134,7 +1310,9 @@ export async function resolveExecutionRunAdapterConfig(input: { responsibleUserId: input.responsibleUserId ?? null, issueId: input.issueId ?? null, heartbeatRunId: input.heartbeatRunId ?? null, - ...(lowTrustAllowedBindingIds !== undefined ? { allowedBindingIds: lowTrustAllowedBindingIds } : {}), + ...(lowTrustAllowedBindingIds !== undefined + ? { allowedBindingIds: lowTrustAllowedBindingIds } + : {}), } : undefined, { adapterType: input.adapterType ?? null }, @@ -1161,7 +1339,9 @@ export async function resolveExecutionRunAdapterConfig(input: { responsibleUserId: input.responsibleUserId ?? null, issueId: input.issueId ?? null, heartbeatRunId: input.heartbeatRunId ?? null, - ...(lowTrustAllowedBindingIds !== undefined ? { allowedBindingIds: lowTrustAllowedBindingIds } : {}), + ...(lowTrustAllowedBindingIds !== undefined + ? { allowedBindingIds: lowTrustAllowedBindingIds } + : {}), } : undefined, ) @@ -1188,7 +1368,9 @@ export async function resolveExecutionRunAdapterConfig(input: { responsibleUserId: input.responsibleUserId ?? null, issueId: input.issueId ?? null, heartbeatRunId: input.heartbeatRunId ?? null, - ...(lowTrustAllowedBindingIds !== undefined ? { allowedBindingIds: lowTrustAllowedBindingIds } : {}), + ...(lowTrustAllowedBindingIds !== undefined + ? { allowedBindingIds: lowTrustAllowedBindingIds } + : {}), } : undefined, ) @@ -1217,7 +1399,10 @@ export async function resolveExecutionRunAdapterConfig(input: { // adapter can probe once the sandbox is up — and on managed cloud hosts a // host-side login never exists at all. The adapter's execute-time gate // remains the authority there; it probes the sandbox before failing. - if ((input.adapterType ?? null) === "codex_local" && (input.environmentDriver ?? null) !== "sandbox") { + if ( + (input.adapterType ?? null) === "codex_local" && + (input.environmentDriver ?? null) !== "sandbox" + ) { const resolvedEnv = parseObject(resolvedConfig.env); const readiness = await evaluateCodexCredentialReadiness({ env: process.env, @@ -1278,11 +1463,7 @@ export function applyRunScopedMentionedSkillKeys( skillKeys: string[], ): Record { const normalizedSkillKeys = Array.from( - new Set( - skillKeys - .map((value) => value.trim()) - .filter(Boolean), - ), + new Set(skillKeys.map((value) => value.trim()).filter(Boolean)), ); if (normalizedSkillKeys.length === 0) return config; @@ -1302,7 +1483,8 @@ export function computeBoundedTransientHeartbeatRetrySchedule( const baseDelayMs = BOUNDED_TRANSIENT_HEARTBEAT_RETRY_DELAYS_MS[attempt - 1]; if (typeof baseDelayMs !== "number") return null; const sample = Math.min(1, Math.max(0, random())); - const jitterMultiplier = 1 + (((sample * 2) - 1) * BOUNDED_TRANSIENT_HEARTBEAT_RETRY_JITTER_RATIO); + const jitterMultiplier = + 1 + (sample * 2 - 1) * BOUNDED_TRANSIENT_HEARTBEAT_RETRY_JITTER_RATIO; const delayMs = Math.max(1_000, Math.round(baseDelayMs * jitterMultiplier)); return { attempt, @@ -1326,7 +1508,9 @@ async function resolveRunScopedMentionedSkillKeys(input: { description: issues.description, }) .from(issues) - .where(and(eq(issues.id, input.issueId), eq(issues.companyId, input.companyId))) + .where( + and(eq(issues.id, input.issueId), eq(issues.companyId, input.companyId)), + ) .then((rows) => rows[0] ?? null); if (!issue) return []; @@ -1383,7 +1567,9 @@ export function applyPersistedExecutionWorkspaceConfig(input: { if (input.workspaceConfig?.workspaceRuntime === null) { delete nextConfig.workspaceRuntime; } else if (input.workspaceConfig?.workspaceRuntime) { - nextConfig.workspaceRuntime = { ...input.workspaceConfig.workspaceRuntime }; + nextConfig.workspaceRuntime = { + ...input.workspaceConfig.workspaceRuntime, + }; } if (input.workspaceConfig?.desiredState === null) { delete nextConfig.desiredState; @@ -1399,11 +1585,16 @@ export function applyPersistedExecutionWorkspaceConfig(input: { if (input.workspaceConfig && input.mode === "isolated_workspace") { const nextStrategy = parseObject(nextConfig.workspaceStrategy); - if (input.workspaceConfig.provisionCommand === null) delete nextStrategy.provisionCommand; + if (input.workspaceConfig.provisionCommand === null) + delete nextStrategy.provisionCommand; else nextStrategy.provisionCommand = input.workspaceConfig.provisionCommand; - if (input.workspaceConfig.runtimeProvisionCommand === null) delete nextStrategy.runtimeProvisionCommand; - else nextStrategy.runtimeProvisionCommand = input.workspaceConfig.runtimeProvisionCommand; - if (input.workspaceConfig.teardownCommand === null) delete nextStrategy.teardownCommand; + if (input.workspaceConfig.runtimeProvisionCommand === null) + delete nextStrategy.runtimeProvisionCommand; + else + nextStrategy.runtimeProvisionCommand = + input.workspaceConfig.runtimeProvisionCommand; + if (input.workspaceConfig.teardownCommand === null) + delete nextStrategy.teardownCommand; else nextStrategy.teardownCommand = input.workspaceConfig.teardownCommand; nextConfig.workspaceStrategy = nextStrategy; } @@ -1429,10 +1620,7 @@ export function mergeExecutionWorkspaceMetadataForPersistence(input: { } as Record; const existingSnapshot = parseObject(base.baseRefSnapshot); - if ( - typeof existingSnapshot.resolvedSha !== "string" - && input.baseRefSha - ) { + if (typeof existingSnapshot.resolvedSha !== "string" && input.baseRefSha) { base.baseRefSnapshot = { baseRef: input.baseRef ?? null, resolvedSha: input.baseRefSha, @@ -1449,14 +1637,19 @@ export function mergeExecutionWorkspaceMetadataForPersistence(input: { }; } - if ((input.shouldReuseExisting && !input.shouldRefreshConfigSnapshot) || !input.configSnapshot) { + if ( + (input.shouldReuseExisting && !input.shouldRefreshConfigSnapshot) || + !input.configSnapshot + ) { return base; } return mergeExecutionWorkspaceConfig(base, input.configSnapshot); } -export function stripWorkspaceRuntimeFromExecutionRunConfig(config: Record) { +export function stripWorkspaceRuntimeFromExecutionRunConfig( + config: Record, +) { const nextConfig = { ...config }; delete nextConfig.workspaceRuntime; return nextConfig; @@ -1477,38 +1670,54 @@ function buildExecutionWorkspaceConfigSnapshot( } if ("workspaceStrategy" in config) { - snapshot.provisionCommand = typeof strategy.provisionCommand === "string" ? strategy.provisionCommand : null; + snapshot.provisionCommand = + typeof strategy.provisionCommand === "string" + ? strategy.provisionCommand + : null; snapshot.runtimeProvisionCommand = - typeof strategy.runtimeProvisionCommand === "string" ? strategy.runtimeProvisionCommand : null; - snapshot.teardownCommand = typeof strategy.teardownCommand === "string" ? strategy.teardownCommand : null; + typeof strategy.runtimeProvisionCommand === "string" + ? strategy.runtimeProvisionCommand + : null; + snapshot.teardownCommand = + typeof strategy.teardownCommand === "string" + ? strategy.teardownCommand + : null; } if ("workspaceRuntime" in config) { const workspaceRuntime = parseObject(config.workspaceRuntime); - snapshot.workspaceRuntime = Object.keys(workspaceRuntime).length > 0 ? workspaceRuntime : null; + snapshot.workspaceRuntime = + Object.keys(workspaceRuntime).length > 0 ? workspaceRuntime : null; } if ("desiredState" in config) { snapshot.desiredState = - config.desiredState === "running" || config.desiredState === "stopped" || config.desiredState === "manual" + config.desiredState === "running" || + config.desiredState === "stopped" || + config.desiredState === "manual" ? config.desiredState : null; } if ("serviceStates" in config) { const serviceStates = parseObject(config.serviceStates); - snapshot.serviceStates = Object.keys(serviceStates).length > 0 - ? Object.fromEntries( - Object.entries(serviceStates).filter(([, state]) => - state === "running" || state === "stopped" || state === "manual" - ), - ) as ExecutionWorkspaceConfig["serviceStates"] - : null; + snapshot.serviceStates = + Object.keys(serviceStates).length > 0 + ? (Object.fromEntries( + Object.entries(serviceStates).filter( + ([, state]) => + state === "running" || + state === "stopped" || + state === "manual", + ), + ) as ExecutionWorkspaceConfig["serviceStates"]) + : null; } - const hasSnapshot = Object.values(snapshot).some((value) => { - if (value === null) return false; - if (typeof value === "object") return Object.keys(value).length > 0; - return true; - }) || hasExplicitEnvironmentSelection; + const hasSnapshot = + Object.values(snapshot).some((value) => { + if (value === null) return false; + if (typeof value === "object") return Object.keys(value).length > 0; + return true; + }) || hasExplicitEnvironmentSelection; return hasSnapshot ? snapshot : null; } @@ -1522,9 +1731,10 @@ export function stripHostWorkspaceProvisionForLowTrustSandbox(input: { const workspaceStrategy = parseObject(input.config.workspaceStrategy); if ( - typeof workspaceStrategy.provisionCommand !== "string" - && typeof workspaceStrategy.runtimeProvisionCommand !== "string" - ) return input.config; + typeof workspaceStrategy.provisionCommand !== "string" && + typeof workspaceStrategy.runtimeProvisionCommand !== "string" + ) + return input.config; const nextWorkspaceStrategy = { ...workspaceStrategy }; delete nextWorkspaceStrategy.provisionCommand; @@ -1541,10 +1751,17 @@ export async function preflightLowTrustWorkspaceIsolation(input: { trustPreset: TrustPresetResolution; isolatedWorkspacesEnabled: boolean; effectiveExecutionWorkspaceMode: string | null | undefined; - issue: { companyId: string; id?: string | null; projectId?: string | null } | null; + issue: { + companyId: string; + id?: string | null; + projectId?: string | null; + } | null; resolveSelectedEnvironmentDriver: () => Promise; }): Promise { - if (input.trustPreset.kind !== "denied" && input.trustPreset.kind !== "low_trust_review") { + if ( + input.trustPreset.kind !== "denied" && + input.trustPreset.kind !== "low_trust_review" + ) { return null; } @@ -1565,15 +1782,24 @@ export async function preflightLowTrustWorkspaceIsolation(input: { return selectedEnvironmentDriver ?? null; } -export async function resolveWorkspaceAfterLowTrustPreflight(input: { +export async function resolveWorkspaceAfterLowTrustPreflight< + TWorkspace, +>(input: { db?: Db; trustPreset: TrustPresetResolution; isolatedWorkspacesEnabled: boolean; effectiveExecutionWorkspaceMode: string | null | undefined; - issue: { companyId: string; id?: string | null; projectId?: string | null } | null; + issue: { + companyId: string; + id?: string | null; + projectId?: string | null; + } | null; resolveSelectedEnvironmentDriver: () => Promise; resolveWorkspace: () => Promise; -}): Promise<{ selectedEnvironmentDriver: string | null; workspace: TWorkspace }> { +}): Promise<{ + selectedEnvironmentDriver: string | null; + workspace: TWorkspace; +}> { const selectedEnvironmentDriver = await preflightLowTrustWorkspaceIsolation({ db: input.db, trustPreset: input.trustPreset, @@ -1595,7 +1821,12 @@ function deriveRepoNameFromRepoUrl(repoUrl: string | null): string | null { try { const parsed = new URL(trimmed); const cleanedPath = parsed.pathname.replace(/\/+$/, ""); - const repoName = cleanedPath.split("/").filter(Boolean).pop()?.replace(/\.git$/i, "") ?? ""; + const repoName = + cleanedPath + .split("/") + .filter(Boolean) + .pop() + ?.replace(/\.git$/i, "") ?? ""; return repoName || null; } catch { return null; @@ -1608,7 +1839,10 @@ function deriveRepoNameFromRepoUrl(repoUrl: string | null): string | null { * clone target — the loser saw "destination path already exists" and its failure cleanup * deleted the winner's in-progress clone, so both runs failed every round. */ -const managedCheckoutMaterializations = new Map>(); +const managedCheckoutMaterializations = new Map< + string, + Promise<{ cwd: string; warning: string | null }> +>(); export async function ensureManagedProjectWorkspace(input: { companyId: string; @@ -1672,44 +1906,59 @@ async function materializeManagedProjectWorkspace( // is never created in a partial state and never removed on failure, so a concurrent // materialization (another process, or a run racing this one) can neither adopt a broken // checkout nor lose its own completed one. - const auth = input.resolveGitAuth ? await input.resolveGitAuth(input.repoUrl) : null; + const auth = input.resolveGitAuth + ? await input.resolveGitAuth(input.repoUrl) + : null; const cloneTmpDir = await fs.mkdtemp(`${cwd}.clone-`); try { - await execFile("git", [...(auth?.configArgs ?? []), "clone", input.repoUrl, cloneTmpDir], { - env: { - // Spread order matters: the sanitizer strips PAPERCLIP_*, which would remove the - // credential-helper token env if it came first. GIT_TERMINAL_PROMPT=0 fails a - // credential-less private clone immediately instead of hanging on a prompt until - // the clone timeout. - ...sanitizeRuntimeServiceBaseEnv(process.env), - GIT_TERMINAL_PROMPT: "0", - ...(auth?.env ?? {}), + await execFile( + "git", + [...(auth?.configArgs ?? []), "clone", input.repoUrl, cloneTmpDir], + { + env: { + // Spread order matters: the sanitizer strips PAPERCLIP_*, which would remove the + // credential-helper token env if it came first. GIT_TERMINAL_PROMPT=0 fails a + // credential-less private clone immediately instead of hanging on a prompt until + // the clone timeout. + ...sanitizeRuntimeServiceBaseEnv(process.env), + GIT_TERMINAL_PROMPT: "0", + ...(auth?.env ?? {}), + }, + timeout: MANAGED_WORKSPACE_GIT_CLONE_TIMEOUT_MS, }, - timeout: MANAGED_WORKSPACE_GIT_CLONE_TIMEOUT_MS, - }); + ); } catch (error) { - await fs.rm(cloneTmpDir, { recursive: true, force: true }).catch(() => undefined); + await fs + .rm(cloneTmpDir, { recursive: true, force: true }) + .catch(() => undefined); const reason = error instanceof Error ? error.message : String(error); const authNote = describeGitAuthFailure({ error: reason, used: auth ? { source: auth.source, secretName: auth.secretName } : null, }); - throw new Error(scrubGitCredentialText( - `Failed to prepare managed checkout for "${input.repoUrl}" at "${cwd}": ${reason}${authNote ? ` ${authNote}` : ""}`, - )); + throw new Error( + scrubGitCredentialText( + `Failed to prepare managed checkout for "${input.repoUrl}" at "${cwd}": ${reason}${authNote ? ` ${authNote}` : ""}`, + ), + ); } try { await fs.rename(cloneTmpDir, cwd); } catch (renameError) { - await fs.rm(cloneTmpDir, { recursive: true, force: true }).catch(() => undefined); + await fs + .rm(cloneTmpDir, { recursive: true, force: true }) + .catch(() => undefined); // The target appearing between the emptiness check and the rename means another // materialization won the race; adopt its checkout instead of failing the run. if (await hasAdoptableGitDir()) { return { cwd, warning: null }; } - const reason = renameError instanceof Error ? renameError.message : String(renameError); - throw new Error(`Failed to move managed checkout into place at "${cwd}": ${reason}`); + const reason = + renameError instanceof Error ? renameError.message : String(renameError); + throw new Error( + `Failed to move managed checkout into place at "${cwd}": ${reason}`, + ); } return { cwd, warning: null }; } @@ -1755,23 +2004,34 @@ export interface ResolveAdditionalProjectWorkspaceDeps { } /** Build the real dependencies for {@link resolveAdditionalProjectWorkspace}. */ -function defaultAdditionalProjectWorkspaceDeps(db: Db): ResolveAdditionalProjectWorkspaceDeps { +function defaultAdditionalProjectWorkspaceDeps( + db: Db, +): ResolveAdditionalProjectWorkspaceDeps { return { loadProjectWorkspaceRows: (companyId, projectId) => db .select() .from(projectWorkspaces) - .where(and(eq(projectWorkspaces.companyId, companyId), eq(projectWorkspaces.projectId, projectId))) + .where( + and( + eq(projectWorkspaces.companyId, companyId), + eq(projectWorkspaces.projectId, projectId), + ), + ) .orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id)), resolveConfiguredOrManagedProjectCwd: (input) => resolveConfiguredOrManagedProjectCwd({ ...input, - resolveGitAuth: input.resolveGitAuth ?? createGitRemoteAuthProvider(db, input.companyId), + resolveGitAuth: + input.resolveGitAuth ?? + createGitRemoteAuthProvider(db, input.companyId), }), ensureManagedProjectWorkspace: (input) => ensureManagedProjectWorkspace({ ...input, - resolveGitAuth: input.resolveGitAuth ?? createGitRemoteAuthProvider(db, input.companyId), + resolveGitAuth: + input.resolveGitAuth ?? + createGitRemoteAuthProvider(db, input.companyId), }), // A realized workspace must hold real content. An empty directory gives the agent an empty // referenced workspace, so treat an empty directory the same as a missing one. @@ -1808,12 +2068,16 @@ export async function resolveAdditionalProjectWorkspace( ): Promise { const { companyId } = input; const projectId = input.project.projectId; - const workspaceRows = await deps.loadProjectWorkspaceRows(companyId, projectId); + const workspaceRows = await deps.loadProjectWorkspaceRows( + companyId, + projectId, + ); for (const workspace of workspaceRows) { // A row realizes real content only through a configured checkout directory or a repository URL // to clone. A row with neither can produce only an empty managed directory, so skip it here. const configuredCwd = readNonEmptyString(workspace.cwd); - const hasConfiguredCwd = Boolean(configuredCwd) && configuredCwd !== REPO_ONLY_CWD_SENTINEL; + const hasConfiguredCwd = + Boolean(configuredCwd) && configuredCwd !== REPO_ONLY_CWD_SENTINEL; if (!hasConfiguredCwd && !readNonEmptyString(workspace.repoUrl)) { continue; } @@ -1839,8 +2103,11 @@ export async function resolveAdditionalProjectWorkspace( // real source: the first workspace row that supplies a repository URL. Without a real source, do // not fabricate an empty managed directory and report success. Throw instead, so the caller drops // only this referenced project and adds a clear warning. - const fallbackRow = workspaceRows.find((row) => readNonEmptyString(row.repoUrl)) ?? null; - const fallbackRepoUrl = fallbackRow ? readNonEmptyString(fallbackRow.repoUrl) : null; + const fallbackRow = + workspaceRows.find((row) => readNonEmptyString(row.repoUrl)) ?? null; + const fallbackRepoUrl = fallbackRow + ? readNonEmptyString(fallbackRow.repoUrl) + : null; if (!fallbackRow || !fallbackRepoUrl) { throw new Error( `Referenced project ${projectId} has no workspace checkout or repository URL to realize.`, @@ -1860,20 +2127,24 @@ export async function resolveAdditionalProjectWorkspace( }; } -type WorkspaceValidationFailureLike = WorkspaceValidationFailure | { - code: typeof WORKSPACE_VALIDATION_FAILURE_CODE; - resultJson: Record; -}; +type WorkspaceValidationFailureLike = + | WorkspaceValidationFailure + | { + code: typeof WORKSPACE_VALIDATION_FAILURE_CODE; + resultJson: Record; + }; -function isWorkspaceValidationFailure(error: unknown): error is WorkspaceValidationFailureLike { +function isWorkspaceValidationFailure( + error: unknown, +): error is WorkspaceValidationFailureLike { if (error instanceof WorkspaceValidationFailure) return true; const maybe = error as { code?: unknown; resultJson?: unknown } | null; return Boolean( maybe && - maybe.code === WORKSPACE_VALIDATION_FAILURE_CODE && - maybe.resultJson && - typeof maybe.resultJson === "object" && - !Array.isArray(maybe.resultJson), + maybe.code === WORKSPACE_VALIDATION_FAILURE_CODE && + maybe.resultJson && + typeof maybe.resultJson === "object" && + !Array.isArray(maybe.resultJson), ); } @@ -1895,7 +2166,13 @@ function stableStringifyForFingerprint(value: unknown): string { } if (value && typeof value === "object") { const rec = value as Record; - return `{${Object.keys(rec).sort().map((key) => `${JSON.stringify(key)}:${stableStringifyForFingerprint(rec[key])}`).join(",")}}`; + return `{${Object.keys(rec) + .sort() + .map( + (key) => + `${JSON.stringify(key)}:${stableStringifyForFingerprint(rec[key])}`, + ) + .join(",")}}`; } return JSON.stringify(value); } @@ -1906,29 +2183,40 @@ function fingerprintFinalizeWorkspaceBranchValidation(input: { inspection: ReturnType; }) { const digest = createHash("sha256") - .update(stableStringifyForFingerprint({ - version: 1, - reason: "git_worktree_branch_incoherence", - issueId: input.issueId, - executionWorkspaceId: input.executionWorkspaceId, - worktreePath: input.inspection.worktreePath ? path.resolve(input.inspection.worktreePath) : null, - repoRoot: input.inspection.repoRoot ? path.resolve(input.inspection.repoRoot) : null, - expectedBranchName: input.inspection.expectedBranchName, - actualBranchName: input.inspection.actualBranchName, - reasonCode: input.inspection.reasonCode, - })) + .update( + stableStringifyForFingerprint({ + version: 1, + reason: "git_worktree_branch_incoherence", + issueId: input.issueId, + executionWorkspaceId: input.executionWorkspaceId, + worktreePath: input.inspection.worktreePath + ? path.resolve(input.inspection.worktreePath) + : null, + repoRoot: input.inspection.repoRoot + ? path.resolve(input.inspection.repoRoot) + : null, + expectedBranchName: input.inspection.expectedBranchName, + actualBranchName: input.inspection.actualBranchName, + reasonCode: input.inspection.reasonCode, + }), + ) .digest("hex"); return `workspace_finalize_branch_mismatch:v1:sha256:${digest}`; } -function isConfigurationIncompleteFailure(error: unknown): error is ConfigurationIncompleteFailure { +function isConfigurationIncompleteFailure( + error: unknown, +): error is ConfigurationIncompleteFailure { return error instanceof ConfigurationIncompleteFailure; } export function isConfigurationIncompleteFailedRun( run: Pick | null | undefined, ) { - return run?.errorCode === CONFIGURATION_INCOMPLETE_FAILURE_CODE || run?.errorCode === "model_not_found"; + return ( + run?.errorCode === CONFIGURATION_INCOMPLETE_FAILURE_CODE || + run?.errorCode === "model_not_found" + ); } async function hasGitMetadata(cwd: string | null | undefined) { @@ -1948,7 +2236,10 @@ async function isGitCheckout(cwd: string | null | undefined) { .catch(() => false); } -function sameResolvedPath(left: string | null | undefined, right: string | null | undefined) { +function sameResolvedPath( + left: string | null | undefined, + right: string | null | undefined, +) { const leftPath = readNonEmptyString(left); const rightPath = readNonEmptyString(right); if (!leftPath || !rightPath) return false; @@ -1968,7 +2259,11 @@ async function hasGitPushRemote(cwd: string | null | undefined) { .catch(() => []); for (const remoteName of remoteNames) { - const pushUrl = await execFile("git", ["remote", "get-url", "--push", remoteName], { cwd: normalized }) + const pushUrl = await execFile( + "git", + ["remote", "get-url", "--push", remoteName], + { cwd: normalized }, + ) .then((result) => readNonEmptyString(result.stdout)) .catch(() => null); if (pushUrl) return true; @@ -1977,7 +2272,9 @@ async function hasGitPushRemote(cwd: string | null | undefined) { } export async function assertGitWorktreeBaseWorkspaceReady(input: { - requestedExecutionWorkspaceMode: ReturnType; + requestedExecutionWorkspaceMode: ReturnType< + typeof resolveExecutionWorkspaceMode + >; config: Record; issue: { id: string; @@ -2013,8 +2310,13 @@ export async function assertGitWorktreeBaseWorkspaceReady(input: { if (strategyType !== "git_worktree") return; const issueLabel = input.issue.identifier ?? input.issue.id; - const remediation = "This task needs a project / project workspace or a reusable execution workspace before it can run."; - const fail = (reason: string, message: string, extra: Record = {}) => { + const remediation = + "This task needs a project / project workspace or a reusable execution workspace before it can run."; + const fail = ( + reason: string, + message: string, + extra: Record = {}, + ) => { throw new WorkspaceValidationFailure(message, { workspaceValidation: { reason, @@ -2023,7 +2325,8 @@ export async function assertGitWorktreeBaseWorkspaceReady(input: { issueProjectId: input.issue!.projectId, issueProjectWorkspaceId: input.issue!.projectWorkspaceId, issueExecutionWorkspaceId: input.issue!.executionWorkspaceId ?? null, - issueExecutionWorkspacePreference: input.issue!.executionWorkspacePreference ?? null, + issueExecutionWorkspacePreference: + input.issue!.executionWorkspacePreference ?? null, requestedExecutionWorkspaceMode: input.requestedExecutionWorkspaceMode, workspaceStrategyType: strategyType, resolvedWorkspaceSource: input.base.source, @@ -2058,7 +2361,7 @@ export async function assertGitWorktreeBaseWorkspaceReady(input: { ); } - if (!await isGitCheckout(input.base.baseCwd)) { + if (!(await isGitCheckout(input.base.baseCwd))) { fail( "git_worktree_base_not_git_checkout", `Issue ${issueLabel} requested ${input.requestedExecutionWorkspaceMode} with git_worktree, but base workspace "${input.base.baseCwd}" is not a git checkout. ${remediation}`, @@ -2142,13 +2445,17 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { }) { if (!GIT_SENSITIVE_LOCAL_ADAPTER_TYPES.has(input.adapterType)) return; - const executionTargetKind = readNonEmptyString((input.executionTarget as { kind?: unknown } | null)?.kind) ?? "local"; + const executionTargetKind = + readNonEmptyString( + (input.executionTarget as { kind?: unknown } | null)?.kind, + ) ?? "local"; if (executionTargetKind !== "local") return; const issue = input.issue; if (!issue) return; - const environmentDriver = readNonEmptyString(input.environmentDriver) ?? "local"; + const environmentDriver = + readNonEmptyString(input.environmentDriver) ?? "local"; const leaseMetadata = parseObject(input.leaseMetadata); const leaseProviderMetadata = parseObject(leaseMetadata.providerMetadata); const leaseRemoteCwd = @@ -2156,14 +2463,20 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { readNonEmptyString(leaseProviderMetadata.remoteCwd); const effectiveCwd = readNonEmptyString(input.executionWorkspace.cwd); - const persistedCwd = readNonEmptyString(input.persistedExecutionWorkspace?.cwd); + const persistedCwd = readNonEmptyString( + input.persistedExecutionWorkspace?.cwd, + ); const agentFallbackCwd = resolveDefaultAgentWorkspaceDir(input.agentId); const workspaceExpectation = Boolean(issue.projectWorkspaceId) || Boolean(input.resolvedWorkspace.workspaceId) || input.executionWorkspace.strategy === "git_worktree"; - const fail = (reason: string, message: string, extra: Record = {}) => { + const fail = ( + reason: string, + message: string, + extra: Record = {}, + ) => { throw new WorkspaceValidationFailure(message, { workspaceValidation: { reason, @@ -2179,13 +2492,19 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { executionWorkspaceCwd: effectiveCwd, executionWorkspaceStrategy: input.executionWorkspace.strategy, executionWorkspaceProjectId: input.executionWorkspace.projectId, - executionWorkspaceProjectWorkspaceId: input.executionWorkspace.workspaceId, - persistedExecutionWorkspaceId: input.persistedExecutionWorkspace?.id ?? null, + executionWorkspaceProjectWorkspaceId: + input.executionWorkspace.workspaceId, + persistedExecutionWorkspaceId: + input.persistedExecutionWorkspace?.id ?? null, persistedWorkspaceCwd: persistedCwd, - persistedWorkspaceStrategy: input.persistedExecutionWorkspace?.strategyType ?? null, - persistedProjectId: input.persistedExecutionWorkspace?.projectId ?? null, - persistedProjectWorkspaceId: input.persistedExecutionWorkspace?.projectWorkspaceId ?? null, - persistedProviderRef: input.persistedExecutionWorkspace?.providerRef ?? null, + persistedWorkspaceStrategy: + input.persistedExecutionWorkspace?.strategyType ?? null, + persistedProjectId: + input.persistedExecutionWorkspace?.projectId ?? null, + persistedProjectWorkspaceId: + input.persistedExecutionWorkspace?.projectWorkspaceId ?? null, + persistedProviderRef: + input.persistedExecutionWorkspace?.providerRef ?? null, ...extra, }, }); @@ -2198,7 +2517,8 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { ); } - if (!input.executionTarget && environmentDriver !== "local" && leaseRemoteCwd) return; + if (!input.executionTarget && environmentDriver !== "local" && leaseRemoteCwd) + return; if (workspaceExpectation && !input.persistedExecutionWorkspace) { fail( @@ -2226,7 +2546,8 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { ); } - const expectedProjectWorkspaceId = issue.projectWorkspaceId ?? input.resolvedWorkspace.workspaceId ?? null; + const expectedProjectWorkspaceId = + issue.projectWorkspaceId ?? input.resolvedWorkspace.workspaceId ?? null; if ( expectedProjectWorkspaceId && input.persistedExecutionWorkspace && @@ -2241,7 +2562,8 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { if ( expectedProjectWorkspaceId && input.persistedExecutionWorkspace?.projectWorkspaceId && - input.persistedExecutionWorkspace.projectWorkspaceId !== expectedProjectWorkspaceId + input.persistedExecutionWorkspace.projectWorkspaceId !== + expectedProjectWorkspaceId ) { fail( "project_workspace_mismatch", @@ -2249,7 +2571,11 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { ); } - if (workspaceExpectation && effectiveCwd && sameResolvedPath(effectiveCwd, agentFallbackCwd)) { + if ( + workspaceExpectation && + effectiveCwd && + sameResolvedPath(effectiveCwd, agentFallbackCwd) + ) { fail( "fallback_agent_home_cwd", `Issue ${issue.identifier ?? issue.id} expected a project workspace, but ${input.adapterType} would launch from agent fallback cwd "${effectiveCwd}".`, @@ -2260,7 +2586,10 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { input.persistedExecutionWorkspace?.strategyType === "git_worktree" && input.persistedExecutionWorkspace.providerRef && effectiveCwd && - !sameResolvedPath(effectiveCwd, input.persistedExecutionWorkspace.providerRef) + !sameResolvedPath( + effectiveCwd, + input.persistedExecutionWorkspace.providerRef, + ) ) { fail( "git_worktree_provider_ref_mismatch", @@ -2268,7 +2597,11 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { ); } - if (workspaceExpectation && effectiveCwd && !await hasGitMetadata(effectiveCwd)) { + if ( + workspaceExpectation && + effectiveCwd && + !(await hasGitMetadata(effectiveCwd)) + ) { fail( "missing_git_metadata", `Issue ${issue.identifier ?? issue.id} expected a git workspace for ${input.adapterType}, but "${effectiveCwd}" has no .git metadata.`, @@ -2291,7 +2624,10 @@ export async function assertGitSensitiveAdapterWorkspaceValid(input: { fail( "git_worktree_branch_mismatch", `Issue ${issue.identifier ?? issue.id} expected git worktree branch "${expectedManagedBranchName}" at "${effectiveCwd}", but ${inspection.reason ?? "the checked-out branch could not be verified"}.`, - { managedGitWorktreeBranch: formatManagedGitWorktreeBranchInspection(inspection) }, + { + managedGitWorktreeBranch: + formatManagedGitWorktreeBranchInspection(inspection), + }, ); } } @@ -2361,24 +2697,66 @@ const heartbeatRunSummaryListColumns = { } as const; const heartbeatRunListContextColumns = { - contextIssueId: sql`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("contextIssueId"), - contextTaskId: sql`${heartbeatRuns.contextSnapshot} ->> 'taskId'`.as("contextTaskId"), - contextTaskKey: sql`${heartbeatRuns.contextSnapshot} ->> 'taskKey'`.as("contextTaskKey"), - contextCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"), - contextWakeCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"), - contextWakeReason: sql`${heartbeatRuns.contextSnapshot} ->> 'wakeReason'`.as("contextWakeReason"), - contextWakeSource: sql`${heartbeatRuns.contextSnapshot} ->> 'wakeSource'`.as("contextWakeSource"), - contextWakeTriggerDetail: sql`${heartbeatRuns.contextSnapshot} ->> 'wakeTriggerDetail'`.as("contextWakeTriggerDetail"), + contextIssueId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("contextIssueId"), + contextTaskId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'taskId'`.as("contextTaskId"), + contextTaskKey: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'taskKey'`.as("contextTaskKey"), + contextCommentId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"), + contextWakeCommentId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as( + "contextWakeCommentId", + ), + contextWakeReason: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'wakeReason'`.as("contextWakeReason"), + contextWakeSource: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'wakeSource'`.as("contextWakeSource"), + contextWakeTriggerDetail: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'wakeTriggerDetail'`.as( + "contextWakeTriggerDetail", + ), } as const; const heartbeatRunListResultColumns = { - resultSummary: sql`left(${heartbeatRuns.resultJson} ->> 'summary', ${HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS})`.as("resultSummary"), - resultResult: sql`left(${heartbeatRuns.resultJson} ->> 'result', ${HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS})`.as("resultResult"), - resultMessage: sql`left(${heartbeatRuns.resultJson} ->> 'message', ${HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS})`.as("resultMessage"), - resultError: sql`left(${heartbeatRuns.resultJson} ->> 'error', ${HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS})`.as("resultError"), - resultTotalCostUsd: sql`${heartbeatRuns.resultJson} ->> 'total_cost_usd'`.as("resultTotalCostUsd"), - resultCostUsd: sql`${heartbeatRuns.resultJson} ->> 'cost_usd'`.as("resultCostUsd"), - resultCostUsdCamel: sql`${heartbeatRuns.resultJson} ->> 'costUsd'`.as("resultCostUsdCamel"), + resultSummary: sql< + string | null + >`left(${heartbeatRuns.resultJson} ->> 'summary', ${HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS})`.as( + "resultSummary", + ), + resultResult: sql< + string | null + >`left(${heartbeatRuns.resultJson} ->> 'result', ${HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS})`.as( + "resultResult", + ), + resultMessage: sql< + string | null + >`left(${heartbeatRuns.resultJson} ->> 'message', ${HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS})`.as( + "resultMessage", + ), + resultError: sql< + string | null + >`left(${heartbeatRuns.resultJson} ->> 'error', ${HEARTBEAT_RUN_RESULT_SUMMARY_MAX_CHARS})`.as( + "resultError", + ), + resultTotalCostUsd: sql< + string | null + >`${heartbeatRuns.resultJson} ->> 'total_cost_usd'`.as("resultTotalCostUsd"), + resultCostUsd: sql< + string | null + >`${heartbeatRuns.resultJson} ->> 'cost_usd'`.as("resultCostUsd"), + resultCostUsdCamel: sql< + string | null + >`${heartbeatRuns.resultJson} ->> 'costUsd'`.as("resultCostUsdCamel"), } as const; const heartbeatRunSafeResultJsonColumn = sql | null>` @@ -2454,8 +2832,14 @@ const heartbeatRunIssueSummaryColumns = { status: heartbeatRuns.status, invocationSource: heartbeatRuns.invocationSource, triggerDetail: heartbeatRuns.triggerDetail, - contextCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"), - contextWakeCommentId: sql`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"), + contextCommentId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"), + contextWakeCommentId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as( + "contextWakeCommentId", + ), startedAt: heartbeatRuns.startedAt, finishedAt: heartbeatRuns.finishedAt, createdAt: heartbeatRuns.createdAt, @@ -2471,7 +2855,9 @@ const heartbeatRunIssueSummaryColumns = { lastOutputSeq: heartbeatRuns.lastOutputSeq, lastOutputStream: heartbeatRuns.lastOutputStream, lastOutputBytes: heartbeatRuns.lastOutputBytes, - issueId: sql`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"), + issueId: sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"), } as const; function appendExcerpt(prev: string, chunk: string) { @@ -2484,14 +2870,18 @@ function truncateRunEventString(value: string) { return `${value.slice(0, MAX_RUN_EVENT_PAYLOAD_STRING_CHARS)}\n[truncated ${omittedChars} chars]`; } -function boundRunEventValue(value: unknown, depth: number, seen: WeakSet): unknown { +function boundRunEventValue( + value: unknown, + depth: number, + seen: WeakSet, +): unknown { if (typeof value === "string") { return truncateRunEventString(value); } if ( - value === null - || typeof value === "number" - || typeof value === "boolean" + value === null || + typeof value === "number" || + typeof value === "boolean" ) { return value; } @@ -2536,7 +2926,10 @@ function boundRunEventValue(value: unknown, depth: number, seen: WeakSet } const out: Record = {}; - for (const [key, entryValue] of entries.slice(0, MAX_RUN_EVENT_PAYLOAD_OBJECT_KEYS)) { + for (const [key, entryValue] of entries.slice( + 0, + MAX_RUN_EVENT_PAYLOAD_OBJECT_KEYS, + )) { out[key] = boundRunEventValue(entryValue, depth + 1, seen); } if (entries.length > MAX_RUN_EVENT_PAYLOAD_OBJECT_KEYS) { @@ -2547,18 +2940,25 @@ function boundRunEventValue(value: unknown, depth: number, seen: WeakSet return out; } -export function boundHeartbeatRunEventPayloadForStorage(payload: Record): Record { +export function boundHeartbeatRunEventPayloadForStorage( + payload: Record, +): Record { const bounded = boundRunEventValue(payload, 0, new WeakSet()); return parseObject(bounded) ?? { _truncated: true }; } function redactInlineBase64ImageData(chunk: string) { - return chunk.replace(INLINE_BASE64_IMAGE_DATA_RE, (_match, prefix: string, data: string, suffix: string) => - `${prefix}[omitted base64 image data: ${data.length} chars]${suffix}`, + return chunk.replace( + INLINE_BASE64_IMAGE_DATA_RE, + (_match, prefix: string, data: string, suffix: string) => + `${prefix}[omitted base64 image data: ${data.length} chars]${suffix}`, ); } -export function compactRunLogChunk(chunk: string, maxChars = MAX_PERSISTED_LOG_CHUNK_CHARS) { +export function compactRunLogChunk( + chunk: string, + maxChars = MAX_PERSISTED_LOG_CHUNK_CHARS, +) { const normalized = redactSensitiveText(redactInlineBase64ImageData(chunk)); if (normalized.length <= maxChars) return normalized; @@ -2570,9 +2970,14 @@ export function compactRunLogChunk(chunk: string, maxChars = MAX_PERSISTED_LOG_C } function normalizeMaxConcurrentRuns(value: unknown) { - const parsed = Math.floor(asNumber(value, HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT)); + const parsed = Math.floor( + asNumber(value, HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT), + ); if (!Number.isFinite(parsed)) return HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT; - return Math.max(HEARTBEAT_MAX_CONCURRENT_RUNS_MIN, Math.min(HEARTBEAT_MAX_CONCURRENT_RUNS_MAX, parsed)); + return Math.max( + HEARTBEAT_MAX_CONCURRENT_RUNS_MIN, + Math.min(HEARTBEAT_MAX_CONCURRENT_RUNS_MAX, parsed), + ); } interface WakeupOptions { @@ -2720,7 +3125,10 @@ export function buildAnchorFallbackWorkspaceNotes(input: { ? `Project workspace path "${firstMissing}" and ${extraMissingCount} other configured path(s) are not available yet. Using fallback workspace "${input.fallbackCwd}" for this run.` : `Project workspace path "${firstMissing}" is not available yet. Using fallback workspace "${input.fallbackCwd}" for this run.`, ); - } else if (input.materializationFailures.length === 0 && !input.hasConfiguredProjectCwd) { + } else if ( + input.materializationFailures.length === 0 && + !input.hasConfiguredProjectCwd + ) { warnings.push( `Project workspace has no local cwd configured. Using fallback workspace "${input.fallbackCwd}" for this run.`, ); @@ -2741,7 +3149,10 @@ export function buildAnchorFallbackWorkspaceNotes(input: { * unchanged in the production default. */ export function buildRunWorkspaceHints( - resolved: Pick, + resolved: Pick< + ResolvedWorkspaceForRun, + "workspaceHints" | "additionalWorkspaces" + >, ): Array> { return [ ...resolved.workspaceHints, @@ -2759,14 +3170,19 @@ type ProjectWorkspaceCandidate = { id: string; }; -export function prioritizeProjectWorkspaceCandidatesForRun( - rows: T[], - preferredWorkspaceId: string | null | undefined, -): T[] { +export function prioritizeProjectWorkspaceCandidatesForRun< + T extends ProjectWorkspaceCandidate, +>(rows: T[], preferredWorkspaceId: string | null | undefined): T[] { if (!preferredWorkspaceId) return rows; - const preferredIndex = rows.findIndex((row) => row.id === preferredWorkspaceId); + const preferredIndex = rows.findIndex( + (row) => row.id === preferredWorkspaceId, + ); if (preferredIndex <= 0) return rows; - return [rows[preferredIndex]!, ...rows.slice(0, preferredIndex), ...rows.slice(preferredIndex + 1)]; + return [ + rows[preferredIndex]!, + ...rows.slice(0, preferredIndex), + ...rows.slice(preferredIndex + 1), + ]; } /** @@ -2777,7 +3193,8 @@ export function prioritizeProjectWorkspaceCandidatesForRun [project.id, project])); @@ -3019,7 +3454,9 @@ export async function resolveRunReferencedProjects( for (const projectId of batchCandidateIds) { const project = byId.get(projectId); if (!project) { - warnings.push(`Referenced project ${projectId} was skipped because it is not available in this company.`); + warnings.push( + `Referenced project ${projectId} was skipped because it is not available in this company.`, + ); failures.push({ projectId, reason: "resolution" }); continue; } @@ -3030,12 +3467,15 @@ export async function resolveRunReferencedProjects( // Hydrate the anchor on its own if the candidate loop never ran (no mentions to co-hydrate it with). if (!anchorHydrated && anchorProjectId) { const hydrated = await projects.listByIds(companyId, [anchorProjectId]); - anchorRecord = hydrated.find((project) => project.id === anchorProjectId) ?? null; + anchorRecord = + hydrated.find((project) => project.id === anchorProjectId) ?? null; anchorHydrated = true; } const anchor: RunReferencedProject | null = - anchorRecord && anchorProjectId ? { projectId: anchorProjectId, project: anchorRecord } : null; + anchorRecord && anchorProjectId + ? { projectId: anchorProjectId, project: anchorRecord } + : null; // The loop already bounds `availableCandidates` to at most `evaluationCap` entries. Any mentions left // un-hydrated past the window (the fan-out cap dropped them before hydration/authorization) are @@ -3073,7 +3513,9 @@ export async function resolveRunReferencedProjects( } if (!allowed) { - warnings.push(`Referenced project ${projectId} was skipped because it is not authorized for this run.`); + warnings.push( + `Referenced project ${projectId} was skipped because it is not authorized for this run.`, + ); failures.push({ projectId, reason: "authorization" }); continue; } @@ -3085,7 +3527,8 @@ export async function resolveRunReferencedProjects( // skipped count includes both the still-unconsidered evaluated candidates and any available // candidates that were dropped before evaluation by the fan-out cap above. if (capReachedAtIndex !== null) { - const skipped = candidates.length - capReachedAtIndex + unevaluatedCandidateCount; + const skipped = + candidates.length - capReachedAtIndex + unevaluatedCandidateCount; warnings.push( `Only the first ${cap} referenced project(s) will be synced for this run; ${skipped} additional referenced project(s) were skipped.`, ); @@ -3103,7 +3546,10 @@ export async function resolveRunReferencedProjects( // dropped before hydration carry their id from the ordered mention set. if (capReachedAtIndex !== null) { for (let index = capReachedAtIndex; index < candidates.length; index++) { - failures.push({ projectId: candidates[index]!.projectId, reason: "resolution" }); + failures.push({ + projectId: candidates[index]!.projectId, + reason: "resolution", + }); } } for (const projectId of allCandidateIds.slice(hydrationCursor)) { @@ -3123,7 +3569,9 @@ export interface ResolveAdditionalRunWorkspacesOptions { projects: Pick, "listByIds">; access: Pick, "decide">; /** Resolve one authorized referenced project to its own workspace cwd (injectable for tests). */ - resolveProjectWorkspace: (project: RunReferencedProject) => Promise; + resolveProjectWorkspace: ( + project: RunReferencedProject, + ) => Promise; maxAdditionalProjects?: number; maxCandidateEvaluations?: number; /** @@ -3182,7 +3630,8 @@ export async function resolveAdditionalRunWorkspaces( // exposes an inaccessible referenced path to the agent. if (opts.executionTargetIsRemote) { const remoteReferencedSyncOpen = - (opts.remoteReferencedSyncEnabled ?? false) && (opts.targetStagesConfined ?? false); + (opts.remoteReferencedSyncEnabled ?? false) && + (opts.targetStagesConfined ?? false); if (!remoteReferencedSyncOpen) { const mentionedIds = await opts.issues.findMentionedProjectIds(issueId, { includeCommentBodies: true, @@ -3194,7 +3643,9 @@ export async function resolveAdditionalRunWorkspaces( // emits its structured sync log. Warn only when the issue actually mentions a project, so a // remote run without any referenced mention stays silent. const droppedProjectIds = [ - ...new Set(mentionedIds.filter((projectId) => projectId !== anchorProjectId)), + ...new Set( + mentionedIds.filter((projectId) => projectId !== anchorProjectId), + ), ]; return { additionalWorkspaces: [], @@ -3204,7 +3655,10 @@ export async function resolveAdditionalRunWorkspaces( "Referenced-project workspaces are available only on a local execution target or a confined sandbox target. This run uses a different remote execution target, so no referenced-project workspace was attached.", ] : [], - failures: droppedProjectIds.map((projectId) => ({ projectId, reason: "staging" as const })), + failures: droppedProjectIds.map((projectId) => ({ + projectId, + reason: "staging" as const, + })), }; } // Fall through: a confined sandbox target with the remote flag on resolves and authorizes the @@ -3213,15 +3667,19 @@ export async function resolveAdditionalRunWorkspaces( // directory. The per-project `project:read` check below still runs against the run actor. } - const referenced = await resolveRunReferencedProjects(issueId, anchorProjectId, { - companyId: opts.companyId, - actor: opts.actor, - issues: opts.issues, - projects: opts.projects, - access: opts.access, - maxAdditionalProjects: opts.maxAdditionalProjects, - maxCandidateEvaluations: opts.maxCandidateEvaluations, - }); + const referenced = await resolveRunReferencedProjects( + issueId, + anchorProjectId, + { + companyId: opts.companyId, + actor: opts.actor, + issues: opts.issues, + projects: opts.projects, + access: opts.access, + maxAdditionalProjects: opts.maxAdditionalProjects, + maxCandidateEvaluations: opts.maxCandidateEvaluations, + }, + ); const additionalWorkspaces: ResolvedAdditionalWorkspace[] = []; const warnings = [...referenced.warnings]; @@ -3267,7 +3725,8 @@ export function buildReferencedProjectRunObservability(input: { failures: readonly ReferencedProjectFailure[]; }): ReferencedProjectRunObservability { return { - referenced_projects_requested: input.syncedProjectIds.length + input.failures.length, + referenced_projects_requested: + input.syncedProjectIds.length + input.failures.length, referenced_projects_synced: input.syncedProjectIds.length, referenced_project_failures: input.failures.map((failure) => ({ project_id: failure.projectId, @@ -3286,7 +3745,10 @@ function readNonEmptyString(value: unknown): string | null { function sanitizeAgentSessionMessageText(value: unknown): string | null { const text = readNonEmptyString(value); if (!text) return null; - const redacted = redactSensitiveText(text).slice(0, MAX_AGENT_SESSION_MESSAGE_CHARS); + const redacted = redactSensitiveText(text).slice( + 0, + MAX_AGENT_SESSION_MESSAGE_CHARS, + ); return redacted.trim().length > 0 ? redacted : null; } @@ -3305,7 +3767,9 @@ type ManagedMcpGatewayRunConfig = { function paperclipApiBaseUrl(): string { const configured = readNonEmptyString(process.env.PAPERCLIP_API_URL); if (!configured) { - throw new Error("PAPERCLIP_API_URL is required to deliver managed runtime MCP servers"); + throw new Error( + "PAPERCLIP_API_URL is required to deliver managed runtime MCP servers", + ); } return configured.replace(/\/+$/, "").replace(/\/api$/, ""); } @@ -3319,12 +3783,14 @@ export async function revokeHeartbeatRunGatewayTokens(input: { await input.db .update(toolMcpGatewayTokens) .set({ revokedAt: now, updatedAt: now }) - .where(and( - eq(toolMcpGatewayTokens.companyId, input.companyId), - eq(toolMcpGatewayTokens.subjectType, "heartbeat_run"), - eq(toolMcpGatewayTokens.subjectId, input.runId), - isNull(toolMcpGatewayTokens.revokedAt), - )); + .where( + and( + eq(toolMcpGatewayTokens.companyId, input.companyId), + eq(toolMcpGatewayTokens.subjectType, "heartbeat_run"), + eq(toolMcpGatewayTokens.subjectId, input.runId), + isNull(toolMcpGatewayTokens.revokedAt), + ), + ); } export async function buildPaperclipRuntimeMcpServers(input: { @@ -3332,39 +3798,48 @@ export async function buildPaperclipRuntimeMcpServers(input: { agent: Pick; runId: string; }): Promise { - const effective = await toolAccessService(input.db).getEffectiveProfilesForAgent( - input.agent.companyId, - input.agent.id, - ); + const effective = await toolAccessService( + input.db, + ).getEffectiveProfilesForAgent(input.agent.companyId, input.agent.id); const permittedConnectionIds = new Set([ ...effective.entries .filter((entry) => entry.effect === "include" && entry.connectionId) .map((entry) => entry.connectionId!), ...effective.allowedTools.map((tool) => tool.connectionId), ]); - const installedConnectionIds = new Set(effective.installedConnections.map((connection) => connection.id)); - const permittedConnections = permittedConnectionIds.size > 0 - ? await input.db - .select({ - id: toolConnections.id, - name: toolConnections.name, - transport: toolConnections.transport, - }) - .from(toolConnections) - .where(and( - eq(toolConnections.companyId, input.agent.companyId), - inArray(toolConnections.id, [...permittedConnectionIds]), - )) - : []; + const installedConnectionIds = new Set( + effective.installedConnections.map((connection) => connection.id), + ); + const permittedConnections = + permittedConnectionIds.size > 0 + ? await input.db + .select({ + id: toolConnections.id, + name: toolConnections.name, + transport: toolConnections.transport, + }) + .from(toolConnections) + .where( + and( + eq(toolConnections.companyId, input.agent.companyId), + inArray(toolConnections.id, [...permittedConnectionIds]), + ), + ) + : []; const permittedNotInstalledConnections = permittedConnections - .filter((connection) => connection.transport === "mcp_remote" && !installedConnectionIds.has(connection.id)) + .filter( + (connection) => + connection.transport === "mcp_remote" && + !installedConnectionIds.has(connection.id), + ) .map(({ id, name }) => ({ id, name })) .sort((a, b) => a.name.localeCompare(b.name)); - const uniqueConnections = effective.installedConnections.filter((connection) => - permittedConnectionIds.has(connection.id) - && connection.status === "active" - && connection.enabled - && connection.transport === "mcp_remote" + const uniqueConnections = effective.installedConnections.filter( + (connection) => + permittedConnectionIds.has(connection.id) && + connection.status === "active" && + connection.enabled && + connection.transport === "mcp_remote", ); const service = createToolGatewayService(input.db); if (uniqueConnections.length === 0) { @@ -3382,19 +3857,27 @@ export async function buildPaperclipRuntimeMcpServers(input: { const [profile] = await input.db .select() .from(toolProfiles) - .where(and(eq(toolProfiles.companyId, connection.companyId), eq(toolProfiles.profileKey, profileKey))) + .where( + and( + eq(toolProfiles.companyId, connection.companyId), + eq(toolProfiles.profileKey, profileKey), + ), + ) .limit(1); if (!profile) continue; const existingGateways = await input.db .select() .from(toolMcpGateways) - .where(and( - eq(toolMcpGateways.companyId, connection.companyId), - eq(toolMcpGateways.status, "active"), - isNull(toolMcpGateways.archivedAt), - )); - let gateway = existingGateways.find((candidate) => - candidate.metadata?.managedRuntimeConnectionId === connection.id + .where( + and( + eq(toolMcpGateways.companyId, connection.companyId), + eq(toolMcpGateways.status, "active"), + isNull(toolMcpGateways.archivedAt), + ), + ); + let gateway = existingGateways.find( + (candidate) => + candidate.metadata?.managedRuntimeConnectionId === connection.id, ); if (!gateway) { const slug = `runtime-${connection.id.replaceAll("-", "")}`; @@ -3420,7 +3903,12 @@ export async function buildPaperclipRuntimeMcpServers(input: { [gateway] = await input.db .select() .from(toolMcpGateways) - .where(and(eq(toolMcpGateways.companyId, connection.companyId), eq(toolMcpGateways.slug, slug))) + .where( + and( + eq(toolMcpGateways.companyId, connection.companyId), + eq(toolMcpGateways.slug, slug), + ), + ) .limit(1); if (!gateway) throw error; } @@ -3484,15 +3972,33 @@ function gatewayAppliesToRun(input: { if (gateway.agentId && gateway.agentId !== agentId) return false; if (gateway.projectId && gateway.projectId !== projectId) return false; if (gateway.issueId && gateway.issueId !== issueId) return false; - if (gateway.contextScopeType === "agent" && gateway.contextScopeId && gateway.contextScopeId !== agentId) return false; - if (gateway.contextScopeType === "project" && gateway.contextScopeId && gateway.contextScopeId !== projectId) return false; - if (gateway.contextScopeType === "issue" && gateway.contextScopeId && gateway.contextScopeId !== issueId) return false; + if ( + gateway.contextScopeType === "agent" && + gateway.contextScopeId && + gateway.contextScopeId !== agentId + ) + return false; + if ( + gateway.contextScopeType === "project" && + gateway.contextScopeId && + gateway.contextScopeId !== projectId + ) + return false; + if ( + gateway.contextScopeType === "issue" && + gateway.contextScopeId && + gateway.contextScopeId !== issueId + ) + return false; return true; } async function createManagedMcpRunConfig(input: { db: Db; - agent: Pick; + agent: Pick< + typeof agents.$inferSelect, + "id" | "companyId" | "name" | "adapterType" + >; runId: string; config: Record; projectId: string | null; @@ -3504,19 +4010,23 @@ async function createManagedMcpRunConfig(input: { const rows = await input.db .select() .from(toolMcpGateways) - .where(and( - eq(toolMcpGateways.companyId, input.agent.companyId), - eq(toolMcpGateways.status, "active"), - isNull(toolMcpGateways.archivedAt), - )) + .where( + and( + eq(toolMcpGateways.companyId, input.agent.companyId), + eq(toolMcpGateways.status, "active"), + isNull(toolMcpGateways.archivedAt), + ), + ) .orderBy(asc(toolMcpGateways.name)); - const gateways = rows.filter((gateway) => gatewayAppliesToRun({ - gateway, - agentId: input.agent.id, - projectId: input.projectId, - issueId: input.issueId, - })); + const gateways = rows.filter((gateway) => + gatewayAppliesToRun({ + gateway, + agentId: input.agent.id, + projectId: input.projectId, + issueId: input.issueId, + }), + ); if (gateways.length === 0) return null; const service = createToolGatewayService(input.db); @@ -3569,8 +4079,13 @@ export function normalizeModelProfileWakeContext(input: { contextSnapshot: Record; payload: Record | null | undefined; }): Record { - const modelProfileFromPayload = readModelProfileKey(input.payload?.modelProfile); - if (!readContextModelProfile(input.contextSnapshot) && modelProfileFromPayload) { + const modelProfileFromPayload = readModelProfileKey( + input.payload?.modelProfile, + ); + if ( + !readContextModelProfile(input.contextSnapshot) && + modelProfileFromPayload + ) { input.contextSnapshot.modelProfile = modelProfileFromPayload; } return input.contextSnapshot; @@ -3579,7 +4094,11 @@ export function normalizeModelProfileWakeContext(input: { function readAgentRuntimeModelProfile( runtimeConfig: unknown, key: ModelProfileKey, -): { enabled: boolean; adapterConfig: Record; configured: boolean } { +): { + enabled: boolean; + adapterConfig: Record; + configured: boolean; +} { const modelProfiles = parseObject(parseObject(runtimeConfig).modelProfiles); const profile = parseObject(modelProfiles[key]); if (Object.keys(profile).length === 0) { @@ -3620,19 +4139,26 @@ export function resolveModelProfileApplication(input: { }; } - const adapterProfile = input.adapterModelProfiles.find((profile) => profile.key === requested) ?? null; + const adapterProfile = + input.adapterModelProfiles.find((profile) => profile.key === requested) ?? + null; if (!adapterProfile) { return { requested, requestedBy, applied: null, configSource: null, - fallbackReason: input.profileResolutionFallbackReason ?? "adapter_profile_not_supported", + fallbackReason: + input.profileResolutionFallbackReason ?? + "adapter_profile_not_supported", adapterConfig: null, }; } - const runtimeProfile = readAgentRuntimeModelProfile(input.agentRuntimeConfig, requested); + const runtimeProfile = readAgentRuntimeModelProfile( + input.agentRuntimeConfig, + requested, + ); if (!runtimeProfile.enabled) { return { requested, @@ -3648,7 +4174,9 @@ export function resolveModelProfileApplication(input: { requested, requestedBy, applied: requested, - configSource: runtimeProfile.configured ? "agent_runtime" : "adapter_default", + configSource: runtimeProfile.configured + ? "agent_runtime" + : "adapter_default", fallbackReason: null, adapterConfig: { ...parseObject(adapterProfile.adapterConfig), @@ -3753,7 +4281,10 @@ export function summarizeHeartbeatRunListResultJson(input: { } function didAutomaticRecoveryFail( - latestRun: Pick | null, + latestRun: Pick< + typeof heartbeatRuns.$inferSelect, + "status" | "contextSnapshot" + > | null, expectedRetryReason: | "assignment_recovery" | "issue_continuation_needed" @@ -3776,7 +4307,10 @@ function isExecutionReviewParticipantRecoveryRun( ) { if (!run) return false; const context = parseObject(run.contextSnapshot); - return readNonEmptyString(context.retryReason) === EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON; + return ( + readNonEmptyString(context.retryReason) === + EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON + ); } function isExecutionReviewParticipantRecoveryEligibleRun( @@ -3813,10 +4347,17 @@ function normalizeLedgerBillingType(value: unknown): BillingType { } function resolveLedgerBiller(result: AdapterExecutionResult): string { - return readNonEmptyString(result.biller) ?? readNonEmptyString(result.provider) ?? "unknown"; + return ( + readNonEmptyString(result.biller) ?? + readNonEmptyString(result.provider) ?? + "unknown" + ); } -function normalizeBilledCostCents(costUsd: number | null | undefined, billingType: BillingType): number { +function normalizeBilledCostCents( + costUsd: number | null | undefined, + billingType: BillingType, +): number { if (billingType === "subscription_included") return 0; if (typeof costUsd !== "number" || !Number.isFinite(costUsd)) return 0; return Math.max(0, Math.round(costUsd * 100)); @@ -3828,7 +4369,10 @@ export function resolveLedgerCostStatus(input: { cachedInputTokens: number; outputTokens: number; }): CostStatus { - const hasTokenUsage = input.inputTokens > 0 || input.cachedInputTokens > 0 || input.outputTokens > 0; + const hasTokenUsage = + input.inputTokens > 0 || + input.cachedInputTokens > 0 || + input.outputTokens > 0; return input.costUsd == null && hasTokenUsage ? "unpriced" : "reported"; } @@ -3837,11 +4381,19 @@ export function resolveCacheAdjustedCostUsd(input: { cacheAdjustedCostUsd?: number | null; }) { const explicit = input.cacheAdjustedCostUsd; - if (typeof explicit === "number" && Number.isFinite(explicit) && explicit >= 0) { + if ( + typeof explicit === "number" && + Number.isFinite(explicit) && + explicit >= 0 + ) { return explicit; } const reported = input.costUsd; - if (typeof reported === "number" && Number.isFinite(reported) && reported >= 0) { + if ( + typeof reported === "number" && + Number.isFinite(reported) && + reported >= 0 + ) { return reported; } return null; @@ -3896,50 +4448,66 @@ export function buildExplicitResumeSessionOverride(input: { taskSession: ResumeSessionRow | null; sessionCodec: AdapterSessionCodec; }) { - const resumeRunSessionIdAfter = truncateDisplayId(input.resumeRunSessionIdAfter); - const resumeRunSessionIdBefore = truncateDisplayId(input.resumeRunSessionIdBefore); + const resumeRunSessionIdAfter = truncateDisplayId( + input.resumeRunSessionIdAfter, + ); + const resumeRunSessionIdBefore = truncateDisplayId( + input.resumeRunSessionIdBefore, + ); const desiredDisplayId = requiresCanonicalSessionIds(input.adapterType) ? isCanonicalSessionIdForAdapter(input.adapterType, resumeRunSessionIdAfter) ? resumeRunSessionIdAfter - : isCanonicalSessionIdForAdapter(input.adapterType, resumeRunSessionIdBefore) + : isCanonicalSessionIdForAdapter( + input.adapterType, + resumeRunSessionIdBefore, + ) ? resumeRunSessionIdBefore : null - : resumeRunSessionIdAfter ?? resumeRunSessionIdBefore; + : (resumeRunSessionIdAfter ?? resumeRunSessionIdBefore); const runSessionParams = requiresCanonicalSessionIds(input.adapterType) ? normalizeResumeParamsForAdapter( input.adapterType, input.sessionCodec.deserialize(input.resumeRunSessionParams ?? null), ) : null; - const runSessionDisplayId = truncateDisplayId(readNonEmptyString(runSessionParams?.sessionId)); + const runSessionDisplayId = truncateDisplayId( + readNonEmptyString(runSessionParams?.sessionId), + ); const taskSessionParams = normalizeResumeParamsForAdapter( input.adapterType, - input.sessionCodec.deserialize(input.taskSession?.sessionParamsJson ?? null), + input.sessionCodec.deserialize( + input.taskSession?.sessionParamsJson ?? null, + ), ); const taskSessionRawDisplayId = input.taskSession?.sessionDisplayId ?? null; const taskSessionDisplayId = truncateDisplayId( requiresCanonicalSessionIds(input.adapterType) - ? readNonEmptyString(taskSessionParams?.sessionId) ?? - (isCanonicalSessionIdForAdapter(input.adapterType, taskSessionRawDisplayId) ? taskSessionRawDisplayId : null) - : taskSessionRawDisplayId ?? - (input.sessionCodec.getDisplayId ? input.sessionCodec.getDisplayId(taskSessionParams) : null) ?? - readNonEmptyString(taskSessionParams?.sessionId), + ? (readNonEmptyString(taskSessionParams?.sessionId) ?? + (isCanonicalSessionIdForAdapter( + input.adapterType, + taskSessionRawDisplayId, + ) + ? taskSessionRawDisplayId + : null)) + : (taskSessionRawDisplayId ?? + (input.sessionCodec.getDisplayId + ? input.sessionCodec.getDisplayId(taskSessionParams) + : null) ?? + readNonEmptyString(taskSessionParams?.sessionId)), ); const canReuseTaskSessionParams = input.taskSession != null && - (!requiresCanonicalSessionIds(input.adapterType) || taskSessionParams != null) && - ( - input.taskSession.lastRunId === input.resumeFromRunId || - (!!desiredDisplayId && taskSessionDisplayId === desiredDisplayId) - ); - const sessionParams = - canReuseTaskSessionParams - ? taskSessionParams - : runSessionParams - ? runSessionParams - : desiredDisplayId - ? { sessionId: desiredDisplayId } - : null; + (!requiresCanonicalSessionIds(input.adapterType) || + taskSessionParams != null) && + (input.taskSession.lastRunId === input.resumeFromRunId || + (!!desiredDisplayId && taskSessionDisplayId === desiredDisplayId)); + const sessionParams = canReuseTaskSessionParams + ? taskSessionParams + : runSessionParams + ? runSessionParams + : desiredDisplayId + ? { sessionId: desiredDisplayId } + : null; const sessionDisplayId = canReuseTaskSessionParams ? taskSessionDisplayId : runSessionParams @@ -3953,11 +4521,16 @@ export function buildExplicitResumeSessionOverride(input: { }; } -function normalizeUsageTotals(usage: UsageSummary | null | undefined): UsageTotals | null { +function normalizeUsageTotals( + usage: UsageSummary | null | undefined, +): UsageTotals | null { if (!usage) return null; return { inputTokens: Math.max(0, Math.floor(asNumber(usage.inputTokens, 0))), - cachedInputTokens: Math.max(0, Math.floor(asNumber(usage.cachedInputTokens, 0))), + cachedInputTokens: Math.max( + 0, + Math.floor(asNumber(usage.cachedInputTokens, 0)), + ), outputTokens: Math.max(0, Math.floor(asNumber(usage.outputTokens, 0))), }; } @@ -3968,15 +4541,24 @@ function readRawUsageTotals(usageJson: unknown): UsageTotals | null { const inputTokens = Math.max( 0, - Math.floor(asNumber(parsed.rawInputTokens, asNumber(parsed.inputTokens, 0))), + Math.floor( + asNumber(parsed.rawInputTokens, asNumber(parsed.inputTokens, 0)), + ), ); const cachedInputTokens = Math.max( 0, - Math.floor(asNumber(parsed.rawCachedInputTokens, asNumber(parsed.cachedInputTokens, 0))), + Math.floor( + asNumber( + parsed.rawCachedInputTokens, + asNumber(parsed.cachedInputTokens, 0), + ), + ), ); const outputTokens = Math.max( 0, - Math.floor(asNumber(parsed.rawOutputTokens, asNumber(parsed.outputTokens, 0))), + Math.floor( + asNumber(parsed.rawOutputTokens, asNumber(parsed.outputTokens, 0)), + ), ); if (inputTokens <= 0 && cachedInputTokens <= 0 && outputTokens <= 0) { @@ -3990,19 +4572,25 @@ function readRawUsageTotals(usageJson: unknown): UsageTotals | null { }; } -function deriveNormalizedUsageDelta(current: UsageTotals | null, previous: UsageTotals | null): UsageTotals | null { +function deriveNormalizedUsageDelta( + current: UsageTotals | null, + previous: UsageTotals | null, +): UsageTotals | null { if (!current) return null; if (!previous) return { ...current }; - const inputTokens = current.inputTokens >= previous.inputTokens - ? current.inputTokens - previous.inputTokens - : current.inputTokens; - const cachedInputTokens = current.cachedInputTokens >= previous.cachedInputTokens - ? current.cachedInputTokens - previous.cachedInputTokens - : current.cachedInputTokens; - const outputTokens = current.outputTokens >= previous.outputTokens - ? current.outputTokens - previous.outputTokens - : current.outputTokens; + const inputTokens = + current.inputTokens >= previous.inputTokens + ? current.inputTokens - previous.inputTokens + : current.inputTokens; + const cachedInputTokens = + current.cachedInputTokens >= previous.cachedInputTokens + ? current.cachedInputTokens - previous.cachedInputTokens + : current.cachedInputTokens; + const outputTokens = + current.outputTokens >= previous.outputTokens + ? current.outputTokens - previous.outputTokens + : current.outputTokens; return { inputTokens: Math.max(0, inputTokens), @@ -4016,8 +4604,11 @@ function formatCount(value: number | null | undefined) { return value.toLocaleString("en-US"); } -export function parseSessionCompactionPolicy(agent: typeof agents.$inferSelect): SessionCompactionPolicy { - return resolveSessionCompactionPolicy(agent.adapterType, agent.runtimeConfig).policy; +export function parseSessionCompactionPolicy( + agent: typeof agents.$inferSelect, +): SessionCompactionPolicy { + return resolveSessionCompactionPolicy(agent.adapterType, agent.runtimeConfig) + .policy; } export function resolveRuntimeSessionParamsForWorkspace(input: { @@ -4026,7 +4617,9 @@ export function resolveRuntimeSessionParamsForWorkspace(input: { resolvedWorkspace: ResolvedWorkspaceForRun; }) { const { agentId, previousSessionParams, resolvedWorkspace } = input; - const previousSessionId = readNonEmptyString(previousSessionParams?.sessionId); + const previousSessionId = readNonEmptyString( + previousSessionParams?.sessionId, + ); const previousCwd = readNonEmptyString(previousSessionParams?.cwd); if (!previousSessionId || !previousCwd) { return { @@ -4060,7 +4653,9 @@ export function resolveRuntimeSessionParamsForWorkspace(input: { warning: null as string | null, }; } - const previousWorkspaceId = readNonEmptyString(previousSessionParams?.workspaceId); + const previousWorkspaceId = readNonEmptyString( + previousSessionParams?.workspaceId, + ); if ( previousWorkspaceId && resolvedWorkspace.workspaceId && @@ -4076,9 +4671,12 @@ export function resolveRuntimeSessionParamsForWorkspace(input: { ...(previousSessionParams ?? {}), cwd: projectCwd, }; - if (resolvedWorkspace.workspaceId) migratedSessionParams.workspaceId = resolvedWorkspace.workspaceId; - if (resolvedWorkspace.repoUrl) migratedSessionParams.repoUrl = resolvedWorkspace.repoUrl; - if (resolvedWorkspace.repoRef) migratedSessionParams.repoRef = resolvedWorkspace.repoRef; + if (resolvedWorkspace.workspaceId) + migratedSessionParams.workspaceId = resolvedWorkspace.workspaceId; + if (resolvedWorkspace.repoUrl) + migratedSessionParams.repoUrl = resolvedWorkspace.repoUrl; + if (resolvedWorkspace.repoRef) + migratedSessionParams.repoRef = resolvedWorkspace.repoRef; return { sessionParams: migratedSessionParams, @@ -4092,8 +4690,10 @@ function parseIssueAssigneeAdapterOverrides( raw: unknown, ): ParsedIssueAssigneeAdapterOverrides | null { const parsed = parseObject(raw); - const modelProfile = MODEL_PROFILE_KEYS.includes(parsed.modelProfile as ModelProfileKey) - ? parsed.modelProfile as ModelProfileKey + const modelProfile = MODEL_PROFILE_KEYS.includes( + parsed.modelProfile as ModelProfileKey, + ) + ? (parsed.modelProfile as ModelProfileKey) : null; const parsedAdapterConfig = parseObject(parsed.adapterConfig); const adapterConfig = @@ -4102,7 +4702,8 @@ function parseIssueAssigneeAdapterOverrides( typeof parsed.useProjectWorkspace === "boolean" ? parsed.useProjectWorkspace : null; - if (!modelProfile && !adapterConfig && useProjectWorkspace === null) return null; + if (!modelProfile && !adapterConfig && useProjectWorkspace === null) + return null; return { modelProfile, adapterConfig, @@ -4197,7 +4798,11 @@ function allowsIssueInteractionWake( contextSnapshot: Record | null | undefined, ) { const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason); - if (!wakeReason || !ISSUE_TREE_CONTROL_INTERACTION_WAKE_REASONS.has(wakeReason)) return false; + if ( + !wakeReason || + !ISSUE_TREE_CONTROL_INTERACTION_WAKE_REASONS.has(wakeReason) + ) + return false; return Boolean(deriveCommentId(contextSnapshot, null)); } @@ -4263,27 +4868,29 @@ export function isZombieRun( */ export function filterZombieCoalesceTarget< T extends { status: string; id: string }, ->( - target: T | null, - tracked: { has(id: string): boolean }, -): T | null { +>(target: T | null, tracked: { has(id: string): boolean }): T | null { return target && isZombieRun(target, tracked) ? null : target; } export function describeSessionResetReason( contextSnapshot: Record | null | undefined, ) { - if (contextSnapshot?.forceFreshSession === true) return "forceFreshSession was requested"; + if (contextSnapshot?.forceFreshSession === true) + return "forceFreshSession was requested"; const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason); if (wakeReason === "issue_assigned") return "wake reason is issue_assigned"; if (wakeReason === EXECUTION_REVIEW_PARTICIPANT_RECOVERY_WAKE_REASON) { return `wake reason is ${EXECUTION_REVIEW_PARTICIPANT_RECOVERY_WAKE_REASON}`; } - if (wakeReason === "execution_approval_requested") return "wake reason is execution_approval_requested"; + if (wakeReason === "execution_approval_requested") + return "wake reason is execution_approval_requested"; // PF-4: paired with shouldResetTaskSessionForWake — keep the reason wording // explicit so run logs make session reuse/reset behavior legible. - if (wakeReason === "heartbeat_timer" && !deriveTaskKey(contextSnapshot, null)) { + if ( + wakeReason === "heartbeat_timer" && + !deriveTaskKey(contextSnapshot, null) + ) { return "wake reason is heartbeat_timer (unscoped timer wake starts fresh)"; } return null; @@ -4304,9 +4911,13 @@ const WORKSPACE_SYNC_CONFLICT_SIGNATURES = [ "lacks these prerequisite commits", ]; -export function isWorkspaceSyncConflictFailure(message: string | null | undefined): boolean { +export function isWorkspaceSyncConflictFailure( + message: string | null | undefined, +): boolean { if (!message) return false; - return WORKSPACE_SYNC_CONFLICT_SIGNATURES.some((signature) => message.includes(signature)); + return WORKSPACE_SYNC_CONFLICT_SIGNATURES.some((signature) => + message.includes(signature), + ); } export function shouldDeferFollowupWakeForSameIssue(input: { @@ -4325,9 +4936,11 @@ export function shouldDeferFollowupWakeForSameIssue(input: { const SESSION_CONFIGURED_MODEL_KEY = "__paperclipConfiguredModel"; const SESSION_CONFIG_FINGERPRINT_KEY = "__paperclipConfigFingerprint"; -const SESSION_CONFIG_FINGERPRINT_VERSION_KEY = "__paperclipConfigFingerprintVersion"; +const SESSION_CONFIG_FINGERPRINT_VERSION_KEY = + "__paperclipConfigFingerprintVersion"; const SESSION_CONFIG_CATEGORIES_KEY = "__paperclipConfigCategories"; -const SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY = "__paperclipConfigCategoryFingerprints"; +const SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY = + "__paperclipConfigCategoryFingerprints"; const PAPERCLIP_SESSION_METADATA_KEYS = new Set([ SESSION_CONFIGURED_MODEL_KEY, SESSION_CONFIG_FINGERPRINT_KEY, @@ -4360,8 +4973,10 @@ const EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES = [ "realization", ] as const; -type EffectiveRunSessionConfigCategory = (typeof EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES)[number]; -type EffectiveRunWorkspaceConfigCategory = (typeof EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES)[number]; +type EffectiveRunSessionConfigCategory = + (typeof EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES)[number]; +type EffectiveRunWorkspaceConfigCategory = + (typeof EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES)[number]; type EffectiveRunSessionConfigMetadata = { version: typeof EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION; @@ -4388,7 +5003,8 @@ export type EffectiveRunWorkspaceConfigMetadata = { evaluatedAt: string; }; -type WorkspaceConfigFreshnessDecisionAction = "create" | "reuse" | "refresh" | "replace"; +type WorkspaceConfigFreshnessDecisionAction = + "create" | "reuse" | "refresh" | "replace"; type ExecutionWorkspaceConfigFreshnessDecision = { action: WorkspaceConfigFreshnessDecisionAction; @@ -4418,10 +5034,13 @@ type ExecutionWorkspaceReuseProvisioningPolicy = { shouldPersistLatestWorkspaceConfigMetadata: boolean; }; -type WorkspaceReuseIssueRef = { - id?: string | null; - identifier?: string | null; -} | null | undefined; +type WorkspaceReuseIssueRef = + | { + id?: string | null; + identifier?: string | null; + } + | null + | undefined; export type ExecutionWorkspaceReuseRequestForIssue = { requestedExecutionWorkspaceId: string | null; @@ -4434,9 +5053,12 @@ export function resolveExecutionWorkspaceReuseRequestForIssue(input: { issueExecutionWorkspacePreference?: string | null; existingExecutionWorkspaceStatus?: string | null; }): ExecutionWorkspaceReuseRequestForIssue { - const requestedExecutionWorkspaceId = readNonEmptyString(input.issueExecutionWorkspaceId); + const requestedExecutionWorkspaceId = readNonEmptyString( + input.issueExecutionWorkspaceId, + ); const requestedShouldReuseExisting = - input.issueExecutionWorkspacePreference === "reuse_existing" && requestedExecutionWorkspaceId !== null; + input.issueExecutionWorkspacePreference === "reuse_existing" && + requestedExecutionWorkspaceId !== null; return { requestedExecutionWorkspaceId, @@ -4455,7 +5077,8 @@ export function resolveExecutionWorkspaceReuseProvisioningPolicy(input: { }): ExecutionWorkspaceReuseProvisioningPolicy { const shouldRestoreExistingWorkspace = input.requestedShouldReuseExisting; const replacementClassDrift = - input.requestedShouldReuseExisting && input.workspaceConfigFreshness.action === "replace"; + input.requestedShouldReuseExisting && + input.workspaceConfigFreshness.action === "replace"; return { shouldRestoreExistingWorkspace, @@ -4468,23 +5091,28 @@ export function resolveExecutionWorkspaceReuseProvisioningPolicy(input: { } function formatInheritedExecutionWorkspaceReuseFailure(input: { - reason: "inherited_workspace_reuse_failed" | "inherited_workspace_reuse_unavailable"; + reason: + | "inherited_workspace_reuse_failed" + | "inherited_workspace_reuse_unavailable"; issueRef: WorkspaceReuseIssueRef; runId: string; executionWorkspaceId: string | null | undefined; workspaceConfigFreshness: ExecutionWorkspaceConfigFreshnessDecision; cause?: unknown; }) { - const issueLabel = input.issueRef?.identifier ?? input.issueRef?.id ?? input.runId; + const issueLabel = + input.issueRef?.identifier ?? input.issueRef?.id ?? input.runId; const workspaceLabel = input.executionWorkspaceId ?? "unknown workspace"; - const causeMessage = input.cause instanceof Error - ? input.cause.message - : input.cause != null - ? String(input.cause) - : null; - const remediation = input.reason === "inherited_workspace_reuse_failed" - ? "Inspect the referenced execution workspace restore/provision logs, repair or unarchive the workspace, or intentionally clear the issue's reuse_existing workspace binding before retrying." - : "Repair or unarchive the referenced execution workspace, or intentionally clear the issue's reuse_existing workspace binding before retrying."; + const causeMessage = + input.cause instanceof Error + ? input.cause.message + : input.cause != null + ? String(input.cause) + : null; + const remediation = + input.reason === "inherited_workspace_reuse_failed" + ? "Inspect the referenced execution workspace restore/provision logs, repair or unarchive the workspace, or intentionally clear the issue's reuse_existing workspace binding before retrying." + : "Repair or unarchive the referenced execution workspace, or intentionally clear the issue's reuse_existing workspace binding before retrying."; const message = causeMessage ? `Issue ${issueLabel} requested inherited execution workspace reuse for ${workspaceLabel}, but the workspace could not be restored because ${causeMessage}.` : `Issue ${issueLabel} requested inherited execution workspace reuse for ${workspaceLabel}, but the workspace could not be restored.`; @@ -4492,7 +5120,9 @@ function formatInheritedExecutionWorkspaceReuseFailure(input: { return `${message} ${remediation}`; } -export async function provisionExecutionWorkspaceForFreshnessDecision(input: { +export async function provisionExecutionWorkspaceForFreshnessDecision< + T extends { warnings?: string[] }, +>(input: { requestedShouldReuseExisting: boolean; existingExecutionWorkspaceId?: string | null; issueRef: WorkspaceReuseIssueRef; @@ -4538,18 +5168,22 @@ export async function provisionExecutionWorkspaceForFreshnessDecision = { +const EFFECTIVE_RUN_SESSION_CONFIG_CATEGORY_LABELS: Record< + EffectiveRunSessionConfigCategory, + string +> = { adapter: "adapter", adapterConfig: "adapter config", agentRuntimeConfig: "agent runtime config", @@ -4572,7 +5209,10 @@ const EFFECTIVE_RUN_SESSION_CONFIG_CATEGORY_LABELS: Record = { +const EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORY_LABELS: Record< + EffectiveRunWorkspaceConfigCategory, + string +> = { mode: "workspace mode", projectWorkspace: "project workspace", strategy: "workspace strategy", @@ -4582,14 +5222,15 @@ const EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORY_LABELS: Record([ - "mode", - "projectWorkspace", - "strategy", - "repo", - "environment", - "realization", -]); +const WORKSPACE_REPLACEMENT_CONFIG_CATEGORIES = + new Set([ + "mode", + "projectWorkspace", + "strategy", + "repo", + "environment", + "realization", + ]); function parseStoredConfigCategoryFingerprints(value: unknown) { const parsed = parseObject(value); @@ -4604,13 +5245,17 @@ function parseStoredConfigCategoryFingerprints(value: unknown) { function readConfigCategoriesFromSessionParams( sessionParams: Record | null | undefined, ) { - const rawCategories = Array.isArray(sessionParams?.[SESSION_CONFIG_CATEGORIES_KEY]) + const rawCategories = Array.isArray( + sessionParams?.[SESSION_CONFIG_CATEGORIES_KEY], + ) ? sessionParams?.[SESSION_CONFIG_CATEGORIES_KEY] : []; return rawCategories.filter( (category): category is EffectiveRunSessionConfigCategory => typeof category === "string" && - (EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES as readonly string[]).includes(category), + (EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES as readonly string[]).includes( + category, + ), ); } @@ -4618,8 +5263,13 @@ function readConfigFingerprintFromSessionParams( sessionParams: Record | null | undefined, ) { if (!sessionParams) return null; - const fingerprint = readNonEmptyString(sessionParams[SESSION_CONFIG_FINGERPRINT_KEY]); - const version = asNumber(sessionParams[SESSION_CONFIG_FINGERPRINT_VERSION_KEY], 0); + const fingerprint = readNonEmptyString( + sessionParams[SESSION_CONFIG_FINGERPRINT_KEY], + ); + const version = asNumber( + sessionParams[SESSION_CONFIG_FINGERPRINT_VERSION_KEY], + 0, + ); if (!fingerprint || version <= 0) return null; return { fingerprint, @@ -4631,8 +5281,12 @@ function readConfigFingerprintFromSessionParams( }; } -function describeEffectiveRunConfigCategories(categories: readonly EffectiveRunSessionConfigCategory[]) { - return categories.map((category) => EFFECTIVE_RUN_SESSION_CONFIG_CATEGORY_LABELS[category]).join(", "); +function describeEffectiveRunConfigCategories( + categories: readonly EffectiveRunSessionConfigCategory[], +) { + return categories + .map((category) => EFFECTIVE_RUN_SESSION_CONFIG_CATEGORY_LABELS[category]) + .join(", "); } function changedEffectiveRunSessionConfigCategories(input: { @@ -4642,7 +5296,9 @@ function changedEffectiveRunSessionConfigCategories(input: { const changed = EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES.filter( (category) => input.previous[category] !== input.next[category], ); - return changed.length > 0 ? changed : [...EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES]; + return changed.length > 0 + ? changed + : [...EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES]; } function parseStoredWorkspaceConfigCategoryFingerprints(value: unknown) { @@ -4660,29 +5316,39 @@ function readWorkspaceConfigCategoriesFromMetadata(value: unknown) { return rawCategories.filter( (category): category is EffectiveRunWorkspaceConfigCategory => typeof category === "string" && - (EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES as readonly string[]).includes(category), + (EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES as readonly string[]).includes( + category, + ), ); } function readWorkspaceConfigFingerprintFromMetadata( metadata: Record | null | undefined, ) { - const raw = parseObject(metadata?.[WORKSPACE_CONFIG_FINGERPRINT_METADATA_KEY]); - const fingerprint = readNonEmptyString(raw.workspaceHash) ?? readNonEmptyString(raw.fingerprint); + const raw = parseObject( + metadata?.[WORKSPACE_CONFIG_FINGERPRINT_METADATA_KEY], + ); + const fingerprint = + readNonEmptyString(raw.workspaceHash) ?? + readNonEmptyString(raw.fingerprint); const version = asNumber(raw.version, 0); if (!fingerprint || version <= 0) return null; return { fingerprint, version, categories: readWorkspaceConfigCategoriesFromMetadata(raw.categories), - categoryFingerprints: parseStoredWorkspaceConfigCategoryFingerprints(raw.categoryFingerprints), + categoryFingerprints: parseStoredWorkspaceConfigCategoryFingerprints( + raw.categoryFingerprints, + ), }; } function describeEffectiveRunWorkspaceConfigCategories( categories: readonly EffectiveRunWorkspaceConfigCategory[], ) { - return categories.map((category) => EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORY_LABELS[category]).join(", "); + return categories + .map((category) => EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORY_LABELS[category]) + .join(", "); } function changedEffectiveRunWorkspaceConfigCategories(input: { @@ -4692,10 +5358,14 @@ function changedEffectiveRunWorkspaceConfigCategories(input: { const changed = EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES.filter( (category) => input.previous[category] !== input.next[category], ); - return changed.length > 0 ? changed : [...EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES]; + return changed.length > 0 + ? changed + : [...EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES]; } -function workspaceConfigFreshnessActionLabel(action: WorkspaceConfigFreshnessDecisionAction) { +function workspaceConfigFreshnessActionLabel( + action: WorkspaceConfigFreshnessDecisionAction, +) { switch (action) { case "refresh": return "refreshed execution workspace config"; @@ -4708,8 +5378,14 @@ function workspaceConfigFreshnessActionLabel(action: WorkspaceConfigFreshnessDec } } -export function buildWorkspaceConfigFreshnessOperation(input: WorkspaceConfigFreshnessOperationInput) { - if (!input.reuseRequested || !input.hasExistingWorkspace || input.decision.reasons.length === 0) { +export function buildWorkspaceConfigFreshnessOperation( + input: WorkspaceConfigFreshnessOperationInput, +) { + if ( + !input.reuseRequested || + !input.hasExistingWorkspace || + input.decision.reasons.length === 0 + ) { return null; } @@ -4717,7 +5393,9 @@ export function buildWorkspaceConfigFreshnessOperation(input: WorkspaceConfigFre (category) => EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORY_LABELS[category], ); const categorySummary = - changedCategoryLabels.length > 0 ? ` (${changedCategoryLabels.join(", ")})` : ""; + changedCategoryLabels.length > 0 + ? ` (${changedCategoryLabels.join(", ")})` + : ""; const reasonSummary = input.decision.reasons.join("; "); return { @@ -4734,15 +5412,16 @@ export function buildWorkspaceConfigFreshnessOperation(input: WorkspaceConfigFre previousWorkspaceId: input.previousWorkspaceId, activeWorkspaceId: input.activeWorkspaceId, }, - system: - `[paperclip] ${workspaceConfigFreshnessActionLabel(input.decision.action)} after config freshness check${categorySummary}: ${reasonSummary}\n`, + system: `[paperclip] ${workspaceConfigFreshnessActionLabel(input.decision.action)} after config freshness check${categorySummary}: ${reasonSummary}\n`, }; } -async function recordWorkspaceConfigFreshnessOperation(input: WorkspaceConfigFreshnessOperationInput & { - recorder: WorkspaceOperationRecorder; - runId: string; -}) { +async function recordWorkspaceConfigFreshnessOperation( + input: WorkspaceConfigFreshnessOperationInput & { + recorder: WorkspaceOperationRecorder; + runId: string; + }, +) { const operation = buildWorkspaceConfigFreshnessOperation(input); if (!operation) return; @@ -4779,12 +5458,16 @@ function sanitizeSecretManifestForConfigFingerprint( envKey: readNonEmptyString(record.envKey), secretId: readNonEmptyString(record.secretId) ?? "", bindingId: readNonEmptyString(record.bindingId), - version: typeof record.version === "number" && Number.isFinite(record.version) - ? record.version - : readNonEmptyString(record.version), + version: + typeof record.version === "number" && Number.isFinite(record.version) + ? record.version + : readNonEmptyString(record.version), provider: readNonEmptyString(record.provider), providerVersionRef: readNonEmptyString(record.providerVersionRef), - outcome: record.outcome === "success" || record.outcome === "failure" ? record.outcome : null, + outcome: + record.outcome === "success" || record.outcome === "failure" + ? record.outcome + : null, }; }); } @@ -4796,10 +5479,11 @@ async function hashFileContentsForConfigFingerprint(filePath: string) { function isPathInsideRoot(input: { rootPath: string; filePath: string }) { const relative = path.relative(input.rootPath, input.filePath); - return relative === "" || ( - relative.length > 0 - && !relative.startsWith("..") - && !path.isAbsolute(relative) + return ( + relative === "" || + (relative.length > 0 && + !relative.startsWith("..") && + !path.isAbsolute(relative)) ); } @@ -4807,17 +5491,26 @@ function resolveRootBoundInstructionsFingerprintPath(input: { instructionsFilePath: string | null; instructionsRootPath: string | null; instructionsEntryFile: string | null; -}): { filePath: string; skippedReason: null } | { filePath: null; skippedReason: string | null } { - if (!input.instructionsRootPath || !path.isAbsolute(input.instructionsRootPath)) { +}): + | { filePath: string; skippedReason: null } + | { filePath: null; skippedReason: string | null } { + if ( + !input.instructionsRootPath || + !path.isAbsolute(input.instructionsRootPath) + ) { return { filePath: null, - skippedReason: input.instructionsFilePath ? "missing_absolute_root" : null, + skippedReason: input.instructionsFilePath + ? "missing_absolute_root" + : null, }; } const rootPath = path.resolve(input.instructionsRootPath); - const candidatePath = input.instructionsEntryFile ?? input.instructionsFilePath; - if (!candidatePath) return { filePath: null, skippedReason: "missing_entry_file" }; + const candidatePath = + input.instructionsEntryFile ?? input.instructionsFilePath; + if (!candidatePath) + return { filePath: null, skippedReason: "missing_entry_file" }; const resolvedPath = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) @@ -4830,33 +5523,46 @@ function resolveRootBoundInstructionsFingerprintPath(input: { return { filePath: resolvedPath, skippedReason: null }; } -async function resolveInstructionsConfigFingerprintMetadata(config: Record) { +async function resolveInstructionsConfigFingerprintMetadata( + config: Record, +) { const instructionsFilePath = readNonEmptyString(config.instructionsFilePath); const instructionsRootPath = readNonEmptyString(config.instructionsRootPath); - const instructionsEntryFile = readNonEmptyString(config.instructionsEntryFile); + const instructionsEntryFile = readNonEmptyString( + config.instructionsEntryFile, + ); const resolved = resolveRootBoundInstructionsFingerprintPath({ instructionsFilePath, instructionsRootPath, instructionsEntryFile, }); - const configuredPath = resolved.filePath ?? instructionsFilePath ?? ( - instructionsRootPath && instructionsEntryFile + const configuredPath = + resolved.filePath ?? + instructionsFilePath ?? + (instructionsRootPath && instructionsEntryFile ? path.resolve(instructionsRootPath, instructionsEntryFile) - : null - ); - if (!configuredPath && !instructionsRootPath && !instructionsEntryFile) return null; + : null); + if (!configuredPath && !instructionsRootPath && !instructionsEntryFile) + return null; const metadata: Record = { configured: true, bundleMode: readNonEmptyString(config.instructionsBundleMode), entryFile: instructionsEntryFile, - pathKind: configuredPath ? (path.isAbsolute(configuredPath) ? "absolute" : "relative") : null, + pathKind: configuredPath + ? path.isAbsolute(configuredPath) + ? "absolute" + : "relative" + : null, readPolicy: "root_bound", }; - if (resolved.skippedReason) metadata.readSkippedReason = resolved.skippedReason; + if (resolved.skippedReason) + metadata.readSkippedReason = resolved.skippedReason; if (resolved.filePath) { try { - metadata.contentHash = await hashFileContentsForConfigFingerprint(resolved.filePath); + metadata.contentHash = await hashFileContentsForConfigFingerprint( + resolved.filePath, + ); metadata.readable = true; } catch { metadata.readable = false; @@ -4881,7 +5587,9 @@ function buildSessionConfigCategoryValues(input: { runtimeSkills: unknown; agentConfigRevision: unknown; }) { - const sanitizedSecretManifest = sanitizeSecretManifestForConfigFingerprint(input.secretManifest); + const sanitizedSecretManifest = sanitizeSecretManifestForConfigFingerprint( + input.secretManifest, + ); const workspaceConfig = { ...parseObject(input.workspaceConfig) }; // issues.updatedAt also advances for comments and status changes. Those are // wake deltas, not execution-workspace configuration changes, so including @@ -4926,7 +5634,9 @@ export async function buildEffectiveRunSessionConfigMetadata(input: { agentConfigRevision?: unknown; }): Promise { const secretManifest = input.secretManifest ?? []; - const instructions = await resolveInstructionsConfigFingerprintMetadata(input.effectiveAdapterConfig); + const instructions = await resolveInstructionsConfigFingerprintMetadata( + input.effectiveAdapterConfig, + ); const categoryValues = buildSessionConfigCategoryValues({ adapterType: input.adapterType, effectiveAdapterConfig: input.effectiveAdapterConfig, @@ -5048,9 +5758,10 @@ export function buildEffectiveRunWorkspaceConfigMetadata(input: { subcategories: EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES, secretManifest, }); - const evaluatedAt = input.evaluatedAt instanceof Date - ? input.evaluatedAt.toISOString() - : readNonEmptyString(input.evaluatedAt) ?? new Date().toISOString(); + const evaluatedAt = + input.evaluatedAt instanceof Date + ? input.evaluatedAt.toISOString() + : (readNonEmptyString(input.evaluatedAt) ?? new Date().toISOString()); return { version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION, fingerprint: fingerprints.workspaceFingerprint.fingerprint, @@ -5081,7 +5792,9 @@ export function resolveExecutionWorkspaceConfigFreshness(input: { }; } - const stored = readWorkspaceConfigFingerprintFromMetadata(input.existingWorkspaceMetadata); + const stored = readWorkspaceConfigFingerprintFromMetadata( + input.existingWorkspaceMetadata, + ); const previous = stored ? { version: stored.version, @@ -5104,7 +5817,9 @@ export function resolveExecutionWorkspaceConfigFreshness(input: { reasons: [], changedCategories: [], storedFingerprint: stored?.fingerprint ?? null, - inferredFingerprint: stored ? null : input.inferredMetadata?.fingerprint ?? null, + inferredFingerprint: stored + ? null + : (input.inferredMetadata?.fingerprint ?? null), nextFingerprint: null, storedFingerprintPresent: Boolean(stored), }; @@ -5115,7 +5830,9 @@ export function resolveExecutionWorkspaceConfigFreshness(input: { action: "replace", shouldReuseExisting: false, shouldRefreshConfigSnapshot: false, - reasons: ["execution workspace configuration fingerprint metadata is missing"], + reasons: [ + "execution workspace configuration fingerprint metadata is missing", + ], changedCategories: [...input.nextMetadata.categories], storedFingerprint: null, inferredFingerprint: null, @@ -5134,7 +5851,9 @@ export function resolveExecutionWorkspaceConfigFreshness(input: { ], changedCategories: [...input.nextMetadata.categories], storedFingerprint: stored?.fingerprint ?? null, - inferredFingerprint: stored ? null : input.inferredMetadata?.fingerprint ?? null, + inferredFingerprint: stored + ? null + : (input.inferredMetadata?.fingerprint ?? null), nextFingerprint: input.nextMetadata.fingerprint, storedFingerprintPresent: Boolean(stored), }; @@ -5145,10 +5864,14 @@ export function resolveExecutionWorkspaceConfigFreshness(input: { action: "reuse", shouldReuseExisting: true, shouldRefreshConfigSnapshot: !stored, - reasons: stored ? [] : ["execution workspace configuration fingerprint metadata is missing"], + reasons: stored + ? [] + : ["execution workspace configuration fingerprint metadata is missing"], changedCategories: [], storedFingerprint: stored?.fingerprint ?? null, - inferredFingerprint: stored ? null : input.inferredMetadata?.fingerprint ?? null, + inferredFingerprint: stored + ? null + : (input.inferredMetadata?.fingerprint ?? null), nextFingerprint: input.nextMetadata.fingerprint, storedFingerprintPresent: Boolean(stored), }; @@ -5159,9 +5882,11 @@ export function resolveExecutionWorkspaceConfigFreshness(input: { next: input.nextMetadata.categoryFingerprints, }); const replacementRequired = changedCategories.some((category) => - WORKSPACE_REPLACEMENT_CONFIG_CATEGORIES.has(category) + WORKSPACE_REPLACEMENT_CONFIG_CATEGORIES.has(category), ); - const action: WorkspaceConfigFreshnessDecisionAction = replacementRequired ? "replace" : "refresh"; + const action: WorkspaceConfigFreshnessDecisionAction = replacementRequired + ? "replace" + : "refresh"; return { action, shouldReuseExisting: action !== "replace", @@ -5171,7 +5896,9 @@ export function resolveExecutionWorkspaceConfigFreshness(input: { ], changedCategories, storedFingerprint: stored?.fingerprint ?? null, - inferredFingerprint: stored ? null : input.inferredMetadata?.fingerprint ?? null, + inferredFingerprint: stored + ? null + : (input.inferredMetadata?.fingerprint ?? null), nextFingerprint: input.nextMetadata.fingerprint, storedFingerprintPresent: Boolean(stored), }; @@ -5195,7 +5922,8 @@ function attachPaperclipSessionMetadataToSessionParams( next[SESSION_CONFIG_FINGERPRINT_KEY] = configMetadata.fingerprint; next[SESSION_CONFIG_FINGERPRINT_VERSION_KEY] = configMetadata.version; next[SESSION_CONFIG_CATEGORIES_KEY] = configMetadata.categories; - next[SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY] = configMetadata.categoryFingerprints; + next[SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY] = + configMetadata.categoryFingerprints; } return next; } @@ -5255,27 +5983,41 @@ export function resolveTaskSessionConfigFreshness(input: { } const reasons: string[] = []; - const storedConfig = readConfigFingerprintFromSessionParams(input.taskSessionParams); - const taskSessionConfiguredModel = readConfiguredModelFromSessionParams(input.taskSessionParams); + const storedConfig = readConfigFingerprintFromSessionParams( + input.taskSessionParams, + ); + const taskSessionConfiguredModel = readConfiguredModelFromSessionParams( + input.taskSessionParams, + ); const modelChangedSinceTaskSession = shouldResetTaskSessionForModelChange({ configuredModel: input.configuredModel, taskSessionParams: input.taskSessionParams, }); if (modelChangedSinceTaskSession && taskSessionConfiguredModel) { - reasons.push(`configured model changed from "${taskSessionConfiguredModel}" to "${input.configuredModel}"`); + reasons.push( + `configured model changed from "${taskSessionConfiguredModel}" to "${input.configuredModel}"`, + ); } let changedCategories: EffectiveRunSessionConfigCategory[] = []; if (input.configMetadata) { if (!storedConfig && !input.preserveLegacySessionWithoutConfigMetadata) { changedCategories = [...input.configMetadata.categories]; - reasons.push("effective run configuration fingerprint metadata is missing"); - } else if (storedConfig && storedConfig.version !== input.configMetadata.version) { + reasons.push( + "effective run configuration fingerprint metadata is missing", + ); + } else if ( + storedConfig && + storedConfig.version !== input.configMetadata.version + ) { changedCategories = [...input.configMetadata.categories]; reasons.push( `effective run configuration fingerprint version changed from ${storedConfig.version} to ${input.configMetadata.version}`, ); - } else if (storedConfig && storedConfig.fingerprint !== input.configMetadata.fingerprint) { + } else if ( + storedConfig && + storedConfig.fingerprint !== input.configMetadata.fingerprint + ) { changedCategories = changedEffectiveRunSessionConfigCategories({ previous: storedConfig.categoryFingerprints, next: input.configMetadata.categoryFingerprints, @@ -5335,11 +6077,17 @@ function shouldQueueFollowupForRunningIssueWake(input: { }) { if (input.wakeCommentId) return true; const wakeReason = readNonEmptyString(input.contextSnapshot?.wakeReason); - return Boolean(wakeReason && RUNNING_ISSUE_WAKE_REASONS_REQUIRING_FOLLOWUP.has(wakeReason)); + return Boolean( + wakeReason && RUNNING_ISSUE_WAKE_REASONS_REQUIRING_FOLLOWUP.has(wakeReason), + ); } function isCheckoutConflictError(error: unknown): boolean { - return error instanceof HttpError && error.status === 409 && error.message === "Issue checkout conflict"; + return ( + error instanceof HttpError && + error.status === 409 && + error.message === "Issue checkout conflict" + ); } function deriveCommentId( @@ -5408,11 +6156,16 @@ function enrichWakeContextSnapshot(input: { payload: Record | null; }) { const { contextSnapshot, reason, source, triggerDetail, payload } = input; - const issueIdFromPayload = readNonEmptyString(payload?.["issueId"]) ?? readNonEmptyString(payload?.["taskId"]); + const issueIdFromPayload = + readNonEmptyString(payload?.["issueId"]) ?? + readNonEmptyString(payload?.["taskId"]); const commentIdFromPayload = readNonEmptyString(payload?.["commentId"]); const taskKey = deriveTaskKey(contextSnapshot, payload); const wakeCommentId = deriveCommentId(contextSnapshot, payload); - const wakeCommentIds = mergeWakeCommentIds(contextSnapshot, commentIdFromPayload); + const wakeCommentIds = mergeWakeCommentIds( + contextSnapshot, + commentIdFromPayload, + ); if (!readNonEmptyString(contextSnapshot["wakeReason"]) && reason) { contextSnapshot.wakeReason = reason; @@ -5426,7 +6179,10 @@ function enrichWakeContextSnapshot(input: { if (!readNonEmptyString(contextSnapshot["taskKey"]) && taskKey) { contextSnapshot.taskKey = taskKey; } - if (!readNonEmptyString(contextSnapshot["commentId"]) && commentIdFromPayload) { + if ( + !readNonEmptyString(contextSnapshot["commentId"]) && + commentIdFromPayload + ) { contextSnapshot.commentId = commentIdFromPayload; } if (wakeCommentIds.length > 0) { @@ -5437,13 +6193,19 @@ function enrichWakeContextSnapshot(input: { // Once comment ids are normalized into the snapshot, rebuild the structured // wake payload from those ids later instead of carrying forward stale data. delete contextSnapshot[PAPERCLIP_WAKE_PAYLOAD_KEY]; - } else if (!readNonEmptyString(contextSnapshot["wakeCommentId"]) && wakeCommentId) { + } else if ( + !readNonEmptyString(contextSnapshot["wakeCommentId"]) && + wakeCommentId + ) { contextSnapshot.wakeCommentId = wakeCommentId; } if (!readNonEmptyString(contextSnapshot["wakeSource"]) && source) { contextSnapshot.wakeSource = source; } - if (!readNonEmptyString(contextSnapshot["wakeTriggerDetail"]) && triggerDetail) { + if ( + !readNonEmptyString(contextSnapshot["wakeTriggerDetail"]) && + triggerDetail + ) { contextSnapshot.wakeTriggerDetail = triggerDetail; } normalizeModelProfileWakeContext({ contextSnapshot, payload }); @@ -5468,18 +6230,26 @@ const INTERACTION_CONTINUATION_CONTEXT_KEYS = [ "newlyResolvedItemIds", ] as const; -function isInteractionResolutionWakePayload(payload: Record | null | undefined) { +function isInteractionResolutionWakePayload( + payload: Record | null | undefined, +) { return readNonEmptyString(payload?.mutation) === "interaction"; } -function clearInteractionContinuationWakeContext(contextSnapshot: Record) { +function clearInteractionContinuationWakeContext( + contextSnapshot: Record, +) { for (const key of INTERACTION_CONTINUATION_CONTEXT_KEYS) { delete contextSnapshot[key]; } } -function hasInteractionContinuationWakeContext(contextSnapshot: Record) { - return INTERACTION_CONTINUATION_CONTEXT_KEYS.some((key) => readNonEmptyString(contextSnapshot[key])); +function hasInteractionContinuationWakeContext( + contextSnapshot: Record, +) { + return INTERACTION_CONTINUATION_CONTEXT_KEYS.some((key) => + readNonEmptyString(contextSnapshot[key]), + ); } function normalizeInteractionContinuationWakeContext( @@ -5517,21 +6287,28 @@ async function resolveAcceptedPlanWakeRoutingDecision(args: { }) .from(issuePlanDecompositions) .innerJoin(issues, eq(issues.id, issuePlanDecompositions.sourceIssueId)) - .where(and( - eq(issuePlanDecompositions.companyId, args.companyId), - eq(issuePlanDecompositions.ownerAgentId, args.agentId), - eq(issuePlanDecompositions.status, "in_flight"), - )) - .orderBy(desc(issuePlanDecompositions.updatedAt), asc(issuePlanDecompositions.createdAt)); + .where( + and( + eq(issuePlanDecompositions.companyId, args.companyId), + eq(issuePlanDecompositions.ownerAgentId, args.agentId), + eq(issuePlanDecompositions.status, "in_flight"), + ), + ) + .orderBy( + desc(issuePlanDecompositions.updatedAt), + asc(issuePlanDecompositions.createdAt), + ); if (activeClaims.length === 0) return null; - if (activeClaims.some((claim) => claim.sourceIssueId === args.issueId)) return null; + if (activeClaims.some((claim) => claim.sourceIssueId === args.issueId)) + return null; const otherActiveClaim = activeClaims[0]; if (!otherActiveClaim) return null; const hasAcceptedContinuationWake = - readNonEmptyString(args.contextSnapshot.interactionKind) === "request_confirmation" && + readNonEmptyString(args.contextSnapshot.interactionKind) === + "request_confirmation" && readNonEmptyString(args.contextSnapshot.interactionStatus) === "accepted"; return { @@ -5552,7 +6329,10 @@ export function mergeCoalescedContextSnapshot( ...existing, ...incoming, }; - if (existing.forceFreshSession === true || incoming.forceFreshSession === true) { + if ( + existing.forceFreshSession === true || + incoming.forceFreshSession === true + ) { merged.forceFreshSession = true; } const mergedCommentIds = mergeWakeCommentIds(existing, incoming); @@ -5575,28 +6355,24 @@ export async function buildPaperclipWakePayload(input: { db: Db; companyId: string; contextSnapshot: Record; - continuationSummary?: - | { - key: string; - title: string | null; - body: string; - sourceTrust?: SourceTrustMetadata | null; - updatedAt: Date; - } - | null; - issueSummary?: - | { - id: string; - identifier: string | null; - title: string; - description: string | null; - status: string; - priority: string; - workMode: string; - projectId?: string | null; - executionPolicy?: unknown; - } - | null; + continuationSummary?: { + key: string; + title: string | null; + body: string; + sourceTrust?: SourceTrustMetadata | null; + updatedAt: Date; + } | null; + issueSummary?: { + id: string; + identifier: string | null; + title: string; + description: string | null; + status: string; + priority: string; + workMode: string; + projectId?: string | null; + executionPolicy?: unknown; + } | null; exposeLowTrustRaw?: boolean; // Experimental: agents write user-interaction content in ASD-STE100 // Simplified Technical English (rendered as a prompt directive downstream). @@ -5604,10 +6380,14 @@ export async function buildPaperclipWakePayload(input: { }) { const executionStage = parseObject(input.contextSnapshot.executionStage); const commentIds = extractWakeCommentIds(input.contextSnapshot); - const annotationCommentId = readNonEmptyString(input.contextSnapshot.annotationCommentId); + const annotationCommentId = readNonEmptyString( + input.contextSnapshot.annotationCommentId, + ); const issueId = readNonEmptyString(input.contextSnapshot.issueId); const continuationSummary = input.continuationSummary ?? null; - const agentMessage = parseObject(input.contextSnapshot[PAPERCLIP_AGENT_MESSAGE_KEY]); + const agentMessage = parseObject( + input.contextSnapshot[PAPERCLIP_AGENT_MESSAGE_KEY], + ); const agentMessageText = sanitizeAgentSessionMessageText(agentMessage.text); const issueSummary = input.issueSummary ?? @@ -5623,15 +6403,18 @@ export async function buildPaperclipWakePayload(input: { workMode: issues.workMode, }) .from(issues) - .where(and(eq(issues.id, issueId), eq(issues.companyId, input.companyId))) + .where( + and(eq(issues.id, issueId), eq(issues.companyId, input.companyId)), + ) .then((rows) => rows[0] ?? null) : null); if ( - commentIds.length === 0 - && Object.keys(executionStage).length === 0 - && !issueSummary - && !agentMessageText - ) return null; + commentIds.length === 0 && + Object.keys(executionStage).length === 0 && + !issueSummary && + !agentMessageText + ) + return null; const commentRows = commentIds.length === 0 @@ -5662,10 +6445,13 @@ export async function buildPaperclipWakePayload(input: { ), ); - const commentsById = new Map(commentRows.map((comment) => [comment.id, comment])); + const commentsById = new Map( + commentRows.map((comment) => [comment.id, comment]), + ); const issueDescription = issueSummary?.description ?? null; const issueDescriptionTruncated = - issueDescription !== null && issueDescription.length > MAX_INLINE_WAKE_ISSUE_DESCRIPTION_CHARS; + issueDescription !== null && + issueDescription.length > MAX_INLINE_WAKE_ISSUE_DESCRIPTION_CHARS; const inlineIssueDescription = issueDescriptionTruncated ? issueDescription.slice(0, MAX_INLINE_WAKE_ISSUE_DESCRIPTION_CHARS) : issueDescription; @@ -5691,15 +6477,24 @@ export async function buildPaperclipWakePayload(input: { } const deletedAt = row.deletedAt ?? null; - const safeRow = deletedAt || input.exposeLowTrustRaw ? row : sanitizeQuarantinedCommentForHigherTrust(row); + const safeRow = + deletedAt || input.exposeLowTrustRaw + ? row + : sanitizeQuarantinedCommentForHigherTrust(row); const fullBody = deletedAt ? "" : safeRow.body; - const allowedBodyChars = Math.min(MAX_INLINE_WAKE_COMMENT_BODY_CHARS, remainingBodyChars); + const allowedBodyChars = Math.min( + MAX_INLINE_WAKE_COMMENT_BODY_CHARS, + remainingBodyChars, + ); if (allowedBodyChars <= 0) { truncated = true; break; } - const body = fullBody.length > allowedBodyChars ? fullBody.slice(0, allowedBodyChars) : fullBody; + const body = + fullBody.length > allowedBodyChars + ? fullBody.slice(0, allowedBodyChars) + : fullBody; const bodyTruncated = body.length < fullBody.length; if (bodyTruncated) truncated = true; remainingBodyChars -= body.length; @@ -5707,16 +6502,18 @@ export async function buildPaperclipWakePayload(input: { comments.push({ id: row.id, issueId: row.issueId, - authorType: row.authorType ?? (row.authorAgentId ? "agent" : row.authorUserId ? "user" : "system"), + authorType: + row.authorType ?? + (row.authorAgentId ? "agent" : row.authorUserId ? "user" : "system"), body, bodyTruncated, - presentation: deletedAt ? null : safeRow.presentation ?? null, - metadata: deletedAt ? null : safeRow.metadata ?? null, + presentation: deletedAt ? null : (safeRow.presentation ?? null), + metadata: deletedAt ? null : (safeRow.metadata ?? null), deletedAt: deletedAt ? deletedAt.toISOString() : null, - deletedByType: deletedAt ? row.deletedByType ?? null : null, - deletedByAgentId: deletedAt ? row.deletedByAgentId ?? null : null, - deletedByUserId: deletedAt ? row.deletedByUserId ?? null : null, - deletedByRunId: deletedAt ? row.deletedByRunId ?? null : null, + deletedByType: deletedAt ? (row.deletedByType ?? null) : null, + deletedByAgentId: deletedAt ? (row.deletedByAgentId ?? null) : null, + deletedByUserId: deletedAt ? (row.deletedByUserId ?? null) : null, + deletedByRunId: deletedAt ? (row.deletedByRunId ?? null) : null, sourceTrust: row.sourceTrust ?? null, createdAt: row.createdAt.toISOString(), author: row.authorAgentId @@ -5727,123 +6524,162 @@ export async function buildPaperclipWakePayload(input: { }); } - const annotationDeltas = annotationCommentId && issueId - ? await input.db - .select({ - id: documentAnnotationComments.id, - issueId: documentAnnotationComments.issueId, - threadId: documentAnnotationComments.threadId, - body: documentAnnotationComments.body, - authorType: documentAnnotationComments.authorType, - authorAgentId: documentAnnotationComments.authorAgentId, - authorUserId: documentAnnotationComments.authorUserId, - createdAt: documentAnnotationComments.createdAt, - documentKey: documentAnnotationThreads.documentKey, - status: documentAnnotationThreads.status, - anchorState: documentAnnotationThreads.anchorState, - anchorConfidence: documentAnnotationThreads.anchorConfidence, - currentRevisionNumber: documentAnnotationThreads.currentRevisionNumber, - selectedText: documentAnnotationThreads.selectedText, - prefixText: documentAnnotationThreads.prefixText, - suffixText: documentAnnotationThreads.suffixText, - }) - .from(documentAnnotationComments) - .innerJoin(documentAnnotationThreads, eq(documentAnnotationComments.threadId, documentAnnotationThreads.id)) - .where(and( - eq(documentAnnotationComments.companyId, input.companyId), - eq(documentAnnotationComments.issueId, issueId), - eq(documentAnnotationComments.id, annotationCommentId), - eq(documentAnnotationThreads.companyId, input.companyId), - eq(documentAnnotationThreads.issueId, issueId), - )) - .then((rows) => rows.map((row) => ({ - id: row.id, - issueId: row.issueId, - threadId: row.threadId, - documentKey: row.documentKey, - revisionNumber: row.currentRevisionNumber, - quote: row.selectedText, - prefix: row.prefixText, - suffix: row.suffixText, - threadStatus: row.status, - anchorState: row.anchorState, - anchorConfidence: row.anchorConfidence, - body: row.body.length > MAX_INLINE_WAKE_COMMENT_BODY_CHARS - ? row.body.slice(0, MAX_INLINE_WAKE_COMMENT_BODY_CHARS) - : row.body, - bodyTruncated: row.body.length > MAX_INLINE_WAKE_COMMENT_BODY_CHARS, - createdAt: row.createdAt.toISOString(), - author: row.authorAgentId - ? { type: "agent", id: row.authorAgentId } - : row.authorUserId - ? { type: "user", id: row.authorUserId } - : { type: row.authorType, id: null }, - }))) - : []; + const annotationDeltas = + annotationCommentId && issueId + ? await input.db + .select({ + id: documentAnnotationComments.id, + issueId: documentAnnotationComments.issueId, + threadId: documentAnnotationComments.threadId, + body: documentAnnotationComments.body, + authorType: documentAnnotationComments.authorType, + authorAgentId: documentAnnotationComments.authorAgentId, + authorUserId: documentAnnotationComments.authorUserId, + createdAt: documentAnnotationComments.createdAt, + documentKey: documentAnnotationThreads.documentKey, + status: documentAnnotationThreads.status, + anchorState: documentAnnotationThreads.anchorState, + anchorConfidence: documentAnnotationThreads.anchorConfidence, + currentRevisionNumber: + documentAnnotationThreads.currentRevisionNumber, + selectedText: documentAnnotationThreads.selectedText, + prefixText: documentAnnotationThreads.prefixText, + suffixText: documentAnnotationThreads.suffixText, + }) + .from(documentAnnotationComments) + .innerJoin( + documentAnnotationThreads, + eq( + documentAnnotationComments.threadId, + documentAnnotationThreads.id, + ), + ) + .where( + and( + eq(documentAnnotationComments.companyId, input.companyId), + eq(documentAnnotationComments.issueId, issueId), + eq(documentAnnotationComments.id, annotationCommentId), + eq(documentAnnotationThreads.companyId, input.companyId), + eq(documentAnnotationThreads.issueId, issueId), + ), + ) + .then((rows) => + rows.map((row) => ({ + id: row.id, + issueId: row.issueId, + threadId: row.threadId, + documentKey: row.documentKey, + revisionNumber: row.currentRevisionNumber, + quote: row.selectedText, + prefix: row.prefixText, + suffix: row.suffixText, + threadStatus: row.status, + anchorState: row.anchorState, + anchorConfidence: row.anchorConfidence, + body: + row.body.length > MAX_INLINE_WAKE_COMMENT_BODY_CHARS + ? row.body.slice(0, MAX_INLINE_WAKE_COMMENT_BODY_CHARS) + : row.body, + bodyTruncated: + row.body.length > MAX_INLINE_WAKE_COMMENT_BODY_CHARS, + createdAt: row.createdAt.toISOString(), + author: row.authorAgentId + ? { type: "agent", id: row.authorAgentId } + : row.authorUserId + ? { type: "user", id: row.authorUserId } + : { type: row.authorType, id: null }, + })), + ) + : []; const interactionId = readNonEmptyString(input.contextSnapshot.interactionId); - const interactionKind = readNonEmptyString(input.contextSnapshot.interactionKind); - const interactionStatus = readNonEmptyString(input.contextSnapshot.interactionStatus); - const checkboxSelection = parseObject(input.contextSnapshot.checkboxSelection); + const interactionKind = readNonEmptyString( + input.contextSnapshot.interactionKind, + ); + const interactionStatus = readNonEmptyString( + input.contextSnapshot.interactionStatus, + ); + const checkboxSelection = parseObject( + input.contextSnapshot.checkboxSelection, + ); const planReviewContext = issueId ? await buildPlanReviewContext({ - db: input.db, - companyId: input.companyId, - issueId, - issueWorkMode: issueSummary?.workMode ?? null, - includeForIssueComment: commentIds.length > 0, - includeForAnnotationDelta: annotationDeltas.length > 0, - interactionId, - }) + db: input.db, + companyId: input.companyId, + issueId, + issueWorkMode: issueSummary?.workMode ?? null, + includeForIssueComment: commentIds.length > 0, + includeForAnnotationDelta: annotationDeltas.length > 0, + interactionId, + }) : null; const documentReviewContext = issueId ? await buildDocumentReviewContext({ - db: input.db, - companyId: input.companyId, - issueId, - includeForIssueComment: commentIds.length > 0, - includeForAnnotationDelta: annotationDeltas.length > 0, - }) + db: input.db, + companyId: input.companyId, + issueId, + includeForIssueComment: commentIds.length > 0, + includeForAnnotationDelta: annotationDeltas.length > 0, + }) : null; - const payloadTruncated = truncated || issueDescriptionTruncated || planReviewContext?.truncated === true || documentReviewContext?.truncated === true; - const recoveryActionId = readNonEmptyString(input.contextSnapshot.recoveryActionId); + const payloadTruncated = + truncated || + issueDescriptionTruncated || + planReviewContext?.truncated === true || + documentReviewContext?.truncated === true; + const recoveryActionId = readNonEmptyString( + input.contextSnapshot.recoveryActionId, + ); const recoveryCause = readNonEmptyString(input.contextSnapshot.recoveryCause); const recoveryAction = recoveryActionId ? await input.db - .select() - .from(issueRecoveryActions) - .where(and( - eq(issueRecoveryActions.id, recoveryActionId), - eq(issueRecoveryActions.companyId, input.companyId), - )) - .then((rows) => rows[0] ?? null) + .select() + .from(issueRecoveryActions) + .where( + and( + eq(issueRecoveryActions.id, recoveryActionId), + eq(issueRecoveryActions.companyId, input.companyId), + ), + ) + .then((rows) => rows[0] ?? null) : null; const recoveryEvidence = parseObject(recoveryAction?.evidence); - const originalAssigneeId = recoveryAction?.returnOwnerAgentId ?? recoveryAction?.previousOwnerAgentId ?? null; + const originalAssigneeId = + recoveryAction?.returnOwnerAgentId ?? + recoveryAction?.previousOwnerAgentId ?? + null; const originalAssignee = originalAssigneeId ? await input.db - .select({ id: agents.id, name: agents.name }) - .from(agents) - .where(and(eq(agents.id, originalAssigneeId), eq(agents.companyId, input.companyId))) - .then((rows) => rows[0] ?? null) + .select({ id: agents.id, name: agents.name }) + .from(agents) + .where( + and( + eq(agents.id, originalAssigneeId), + eq(agents.companyId, input.companyId), + ), + ) + .then((rows) => rows[0] ?? null) : null; const payload = { reason: readNonEmptyString(input.contextSnapshot.wakeReason), - recovery: recoveryAction || recoveryCause - ? { - cause: recoveryAction?.cause ?? recoveryCause, - failureSummary: readNonEmptyString(recoveryEvidence.failureSummary), - originalAssignee: originalAssignee - ? { id: originalAssignee.id, name: originalAssignee.name } - : originalAssigneeId - ? { id: originalAssigneeId, name: null } - : null, - attemptCount: recoveryAction?.attemptCount ?? null, - maxAttempts: recoveryAction?.maxAttempts ?? null, - nextAction: recoveryAction?.nextAction ?? null, - routingFallbackReason: readNonEmptyString(recoveryEvidence.routingFallbackReason), - } - : null, + recovery: + recoveryAction || recoveryCause + ? { + cause: recoveryAction?.cause ?? recoveryCause, + failureSummary: readNonEmptyString(recoveryEvidence.failureSummary), + originalAssignee: originalAssignee + ? { id: originalAssignee.id, name: originalAssignee.name } + : originalAssigneeId + ? { id: originalAssigneeId, name: null } + : null, + attemptCount: recoveryAction?.attemptCount ?? null, + maxAttempts: recoveryAction?.maxAttempts ?? null, + nextAction: recoveryAction?.nextAction ?? null, + routingFallbackReason: readNonEmptyString( + recoveryEvidence.routingFallbackReason, + ), + } + : null, issue: issueSummary ? { id: issueSummary.id, @@ -5864,38 +6700,65 @@ export async function buildPaperclipWakePayload(input: { sessionId: readNonEmptyString(agentMessage.sessionId), } : null, - childIssueSummaries: Array.isArray(input.contextSnapshot.childIssueSummaries) + childIssueSummaries: Array.isArray( + input.contextSnapshot.childIssueSummaries, + ) ? input.contextSnapshot.childIssueSummaries : [], - childIssueSummaryTruncated: input.contextSnapshot.childIssueSummaryTruncated === true, - livenessContinuation: readNonEmptyString(input.contextSnapshot.livenessContinuationState) || - readNonEmptyString(input.contextSnapshot.livenessContinuationInstruction) || - readNonEmptyString(input.contextSnapshot.livenessContinuationSourceRunId) || + childIssueSummaryTruncated: + input.contextSnapshot.childIssueSummaryTruncated === true, + livenessContinuation: + readNonEmptyString(input.contextSnapshot.livenessContinuationState) || + readNonEmptyString( + input.contextSnapshot.livenessContinuationInstruction, + ) || + readNonEmptyString( + input.contextSnapshot.livenessContinuationSourceRunId, + ) || typeof input.contextSnapshot.livenessContinuationAttempt === "number" - ? { - attempt: input.contextSnapshot.livenessContinuationAttempt, - maxAttempts: input.contextSnapshot.livenessContinuationMaxAttempts, - sourceRunId: readNonEmptyString(input.contextSnapshot.livenessContinuationSourceRunId), - state: readNonEmptyString(input.contextSnapshot.livenessContinuationState), - reason: readNonEmptyString(input.contextSnapshot.livenessContinuationReason), - instruction: readNonEmptyString(input.contextSnapshot.livenessContinuationInstruction), - } - : null, + ? { + attempt: input.contextSnapshot.livenessContinuationAttempt, + maxAttempts: input.contextSnapshot.livenessContinuationMaxAttempts, + sourceRunId: readNonEmptyString( + input.contextSnapshot.livenessContinuationSourceRunId, + ), + state: readNonEmptyString( + input.contextSnapshot.livenessContinuationState, + ), + reason: readNonEmptyString( + input.contextSnapshot.livenessContinuationReason, + ), + instruction: readNonEmptyString( + input.contextSnapshot.livenessContinuationInstruction, + ), + } + : null, interactionKind, interactionStatus, - checkboxSelection: Object.keys(checkboxSelection).length > 0 ? checkboxSelection : null, - checkedOutByHarness: input.contextSnapshot[PAPERCLIP_HARNESS_CHECKOUT_KEY] === true, + checkboxSelection: + Object.keys(checkboxSelection).length > 0 ? checkboxSelection : null, + checkedOutByHarness: + input.contextSnapshot[PAPERCLIP_HARNESS_CHECKOUT_KEY] === true, simplifiedEnglishInteractions: input.simplifiedEnglishInteractions === true, - dependencyBlockedInteraction: input.contextSnapshot.dependencyBlockedInteraction === true, + dependencyBlockedInteraction: + input.contextSnapshot.dependencyBlockedInteraction === true, treeHoldInteraction: input.contextSnapshot.treeHoldInteraction === true, activeTreeHold: parseObject(input.contextSnapshot.activeTreeHold), - unresolvedBlockerIssueIds: Array.isArray(input.contextSnapshot.unresolvedBlockerIssueIds) - ? input.contextSnapshot.unresolvedBlockerIssueIds.filter((value): value is string => typeof value === "string" && value.length > 0) + unresolvedBlockerIssueIds: Array.isArray( + input.contextSnapshot.unresolvedBlockerIssueIds, + ) + ? input.contextSnapshot.unresolvedBlockerIssueIds.filter( + (value): value is string => + typeof value === "string" && value.length > 0, + ) : [], - unresolvedBlockerSummaries: Array.isArray(input.contextSnapshot.unresolvedBlockerSummaries) + unresolvedBlockerSummaries: Array.isArray( + input.contextSnapshot.unresolvedBlockerSummaries, + ) ? input.contextSnapshot.unresolvedBlockerSummaries : [], - executionStage: Object.keys(executionStage).length > 0 ? executionStage : null, + executionStage: + Object.keys(executionStage).length > 0 ? executionStage : null, taskWatchdog: (input.contextSnapshot.taskWatchdog ?? null) as unknown, skillTest: (input.contextSnapshot.paperclipSkillTest ?? null) as unknown, continuationSummary: safeContinuationSummary @@ -5926,12 +6789,19 @@ export async function buildPaperclipWakePayload(input: { fallbackFetchNeeded: payloadTruncated || missingCommentCount > 0, }; return issueId - ? createRunSecretRedactionRegistry(input.db).redactForIssue(input.companyId, issueId, payload) + ? createRunSecretRedactionRegistry(input.db).redactForIssue( + input.companyId, + issueId, + payload, + ) : payload; } function runTaskKey(run: typeof heartbeatRuns.$inferSelect) { - return deriveTaskKey(run.contextSnapshot as Record | null, null); + return deriveTaskKey( + run.contextSnapshot as Record | null, + null, + ); } function isSameTaskScope(left: string | null, right: string | null) { @@ -5981,7 +6851,9 @@ export function buildHeartbeatRunStatusLiveEventPayload( }; } -function isHeartbeatRunRuntimeStatusActive(status: string | null | undefined): boolean { +function isHeartbeatRunRuntimeStatusActive( + status: string | null | undefined, +): boolean { return status === "queued" || status === "running"; } @@ -6004,7 +6876,9 @@ function readRuntimeStatusIssueIdCandidate( return undefined; } -function decorateHeartbeatRunRuntimeStatus( +function decorateHeartbeatRunRuntimeStatus< + T extends HeartbeatRunRuntimeStatusRunLike, +>( run: T, expected: { companyId?: string | null; @@ -6025,7 +6899,9 @@ function decorateHeartbeatRunRuntimeStatus, + run: Pick< + typeof heartbeatRuns.$inferSelect, + "id" | "companyId" | "agentId" | "status" | "contextSnapshot" + >, update: RuntimeStatusUpdate, issueId: string | null, ) { @@ -6088,7 +6967,8 @@ function recordHeartbeatRunRuntimeProgress( phase: update.phase as HeartbeatRunStatusPhase, message: update.message, currentToolName: readNonEmptyString(update.currentToolName) ?? null, - lastAssistantSnippet: readNonEmptyString(update.lastAssistantSnippet) ?? null, + lastAssistantSnippet: + readNonEmptyString(update.lastAssistantSnippet) ?? null, lastEventAt: update.lastEventAt ? new Date(update.lastEventAt) : new Date(), }); if (!status) return null; @@ -6097,7 +6977,10 @@ function recordHeartbeatRunRuntimeProgress( return status; } -function sanitizeLiveRunProgressText(value: string, maxChars: number): string | null { +function sanitizeLiveRunProgressText( + value: string, + maxChars: number, +): string | null { const normalized = value.replace(/\s+/g, " ").trim(); if (!normalized) return null; const redacted = redactSensitiveText(normalized); @@ -6105,11 +6988,19 @@ function sanitizeLiveRunProgressText(value: string, maxChars: number): string | return `${redacted.slice(0, maxChars - 3)}...`; } -function readLiveRunProgressString(value: unknown, maxChars: number): string | null { - return typeof value === "string" ? sanitizeLiveRunProgressText(value, maxChars) : null; +function readLiveRunProgressString( + value: unknown, + maxChars: number, +): string | null { + return typeof value === "string" + ? sanitizeLiveRunProgressText(value, maxChars) + : null; } -function readFirstLiveRunProgressString(maxChars: number, values: unknown[]): string | null { +function readFirstLiveRunProgressString( + maxChars: number, + values: unknown[], +): string | null { for (const value of values) { const text = readLiveRunProgressString(value, maxChars); if (text) return text; @@ -6117,25 +7008,35 @@ function readFirstLiveRunProgressString(maxChars: number, values: unknown[]): st return null; } -function readLiveRunToolName(payload: Record | null, eventType: string): string | null { - const toolCall = parseObject(payload?.tool_call) ?? parseObject(payload?.toolCall); +function readLiveRunToolName( + payload: Record | null, + eventType: string, +): string | null { + const toolCall = + parseObject(payload?.tool_call) ?? parseObject(payload?.toolCall); const message = parseObject(payload?.message); - const direct = readFirstLiveRunProgressString(MAX_HEARTBEAT_RUN_RUNTIME_TOOL_NAME_CHARS, [ - payload?.toolName, - payload?.tool_name, - payload?.tool, - payload?.name, - payload?.title, - toolCall?.name, - toolCall?.toolName, - message?.name, - message?.toolName, - ]); + const direct = readFirstLiveRunProgressString( + MAX_HEARTBEAT_RUN_RUNTIME_TOOL_NAME_CHARS, + [ + payload?.toolName, + payload?.tool_name, + payload?.tool, + payload?.name, + payload?.title, + toolCall?.name, + toolCall?.toolName, + message?.name, + message?.toolName, + ], + ); if (direct) return direct; const normalizedEventType = eventType.toLowerCase(); if (!normalizedEventType.includes("tool")) return null; - return readLiveRunProgressString(eventType.replace(/[._-]+/g, " "), MAX_HEARTBEAT_RUN_RUNTIME_TOOL_NAME_CHARS); + return readLiveRunProgressString( + eventType.replace(/[._-]+/g, " "), + MAX_HEARTBEAT_RUN_RUNTIME_TOOL_NAME_CHARS, + ); } function readLiveRunAssistantSnippet( @@ -6145,15 +7046,18 @@ function readLiveRunAssistantSnippet( ): string | null { const normalizedEventType = eventType.toLowerCase(); const messagePayload = parseObject(payload?.message); - const direct = readFirstLiveRunProgressString(MAX_HEARTBEAT_RUN_RUNTIME_ASSISTANT_SNIPPET_CHARS, [ - payload?.text, - payload?.delta, - payload?.text_delta, - payload?.content, - payload?.summary, - messagePayload?.text, - messagePayload?.content, - ]); + const direct = readFirstLiveRunProgressString( + MAX_HEARTBEAT_RUN_RUNTIME_ASSISTANT_SNIPPET_CHARS, + [ + payload?.text, + payload?.delta, + payload?.text_delta, + payload?.content, + payload?.summary, + messagePayload?.text, + messagePayload?.content, + ], + ); if (direct) return direct; if ( @@ -6162,7 +7066,12 @@ function readLiveRunAssistantSnippet( normalizedEventType.includes("message.delta") || normalizedEventType.includes("message_delta") ) { - return message ? sanitizeLiveRunProgressText(message, MAX_HEARTBEAT_RUN_RUNTIME_ASSISTANT_SNIPPET_CHARS) : null; + return message + ? sanitizeLiveRunProgressText( + message, + MAX_HEARTBEAT_RUN_RUNTIME_ASSISTANT_SNIPPET_CHARS, + ) + : null; } return null; @@ -6175,22 +7084,31 @@ function buildRunEventRuntimeProgress(input: { at: Date; }) { const normalizedEventType = input.eventType.toLowerCase(); - if (normalizedEventType === "lifecycle" || normalizedEventType === "adapter.invoke") { + if ( + normalizedEventType === "lifecycle" || + normalizedEventType === "adapter.invoke" + ) { return null; } const currentToolName = readLiveRunToolName(input.payload, input.eventType); - const lastAssistantSnippet = readLiveRunAssistantSnippet(input.payload, input.eventType, input.message); + const lastAssistantSnippet = readLiveRunAssistantSnippet( + input.payload, + input.eventType, + input.message, + ); const fallbackMessage = - readLiveRunProgressString(input.message, MAX_HEARTBEAT_RUN_RUNTIME_ASSISTANT_SNIPPET_CHARS) ?? + readLiveRunProgressString( + input.message, + MAX_HEARTBEAT_RUN_RUNTIME_ASSISTANT_SNIPPET_CHARS, + ) ?? sanitizeLiveRunProgressText( input.eventType.replace(/[._-]+/g, " "), MAX_HEARTBEAT_RUN_RUNTIME_ASSISTANT_SNIPPET_CHARS, ); - const message = - currentToolName - ? `Using ${currentToolName}` - : lastAssistantSnippet ?? fallbackMessage; + const message = currentToolName + ? `Using ${currentToolName}` + : (lastAssistantSnippet ?? fallbackMessage); if (!message) return null; return { @@ -6244,11 +7162,10 @@ export function buildPaperclipTaskMarkdown(input: { const wakeComment = input.wakeComment ?? null; const acceptedPlanContinuation = !wakeComment && - (input.acceptedPlanContinuation || ( - input.interaction?.kind === "request_confirmation" && - input.interaction.status === "accepted" && - issue?.workMode === "planning" - )); + (input.acceptedPlanContinuation || + (input.interaction?.kind === "request_confirmation" && + input.interaction.status === "accepted" && + issue?.workMode === "planning")); if (!issue && !wakeComment) return null; const lines = [ @@ -6268,12 +7185,15 @@ export function buildPaperclipTaskMarkdown(input: { "Answer the question directly in the issue thread. Do not write implementation code, and do not produce an implementation plan. Use tools only for investigation or temporary scratch work when needed; the deliverable is the answer.", ); } else if (issue.workMode === "planning") { - let directive = "Make the plan only. Do not write code or perform implementation work."; + let directive = + "Make the plan only. Do not write code or perform implementation work."; if (wakeComment) { - directive = "Update the plan only. Do not write code or perform implementation work."; + directive = + "Update the plan only. Do not write code or perform implementation work."; } if (acceptedPlanContinuation) { - directive = "Create child issues from the approved plan only. Do not write code or perform implementation work on the planning issue."; + directive = + "Create child issues from the approved plan only. Do not write code or perform implementation work on the planning issue."; } lines.push( `- Work mode: ${quoteTaskScalar("planning")}`, @@ -6295,7 +7215,8 @@ export function buildPaperclipTaskMarkdown(input: { "Create child issues from the approved plan only. Do not write code or perform implementation work on the source issue.", ); } - const description = input.includeDescription === false ? "" : issue.description?.trim(); + const description = + input.includeDescription === false ? "" : issue.description?.trim(); if (description) { lines.push("", "Issue description:", fenceTaskText(description)); } @@ -6307,10 +7228,14 @@ export function buildPaperclipTaskMarkdown(input: { const status = ancestor.status ? ` (${ancestor.status})` : ""; const priority = ancestor.priority ? ` [${ancestor.priority}]` : ""; const title = ancestor.title ? ` ${ancestor.title}` : ""; - lines.push(`- ${index === 0 ? "Parent" : `Ancestor ${index + 1}`}: ${label}${title}${status}${priority}`); + lines.push( + `- ${index === 0 ? "Parent" : `Ancestor ${index + 1}`}: ${label}${title}${status}${priority}`, + ); } if ((input.ancestors ?? []).length > ancestors.length) { - lines.push(`- [ancestor context truncated after ${ancestors.length} entries]`); + lines.push( + `- [ancestor context truncated after ${ancestors.length} entries]`, + ); } } if (wakeComment?.body.trim()) { @@ -6331,7 +7256,8 @@ export function buildPaperclipTaskMarkdown(input: { // On Linux, PIDs can be recycled, so this is a best-effort signal rather // than proof that the original child is still alive. function isProcessAlive(pid: number | null | undefined) { - if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false; + if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) + return false; try { process.kill(pid, 0); return true; @@ -6354,7 +7280,9 @@ export async function persistHeartbeatRunProcessMetadata( .set({ processPid: meta.pid, processGroupId: meta.processGroupId, - processStartedAt: Number.isNaN(startedAt.getTime()) ? new Date() : startedAt, + processStartedAt: Number.isNaN(startedAt.getTime()) + ? new Date() + : startedAt, updatedAt: new Date(), }) .where(eq(heartbeatRuns.id, runId)) @@ -6378,7 +7306,9 @@ async function terminateHeartbeatRunProcess(input: { ? pid : (processGroupId ?? 0), processGroupId: - typeof processGroupId === "number" && Number.isInteger(processGroupId) && processGroupId > 0 + typeof processGroupId === "number" && + Number.isInteger(processGroupId) && + processGroupId > 0 ? processGroupId : null, }, @@ -6386,10 +7316,13 @@ async function terminateHeartbeatRunProcess(input: { ); } -function buildProcessLossMessage(run: { - processPid: number | null; - processGroupId: number | null; -}, options?: { descendantOnly?: boolean }) { +function buildProcessLossMessage( + run: { + processPid: number | null; + processGroupId: number | null; + }, + options?: { descendantOnly?: boolean }, +) { if (options?.descendantOnly && run.processGroupId) { return `Process lost -- parent pid ${run.processPid ?? "unknown"} exited, but descendant process group ${run.processGroupId} was still alive and was terminated`; } @@ -6402,10 +7335,13 @@ function buildProcessLossMessage(run: { return "Process lost -- server may have restarted"; } -function readHotRestartAdoptionMetadata(resultJson: Record | null | undefined) { +function readHotRestartAdoptionMetadata( + resultJson: Record | null | undefined, +) { const result = parseObject(resultJson); const hotRestart = parseObject(result.hotRestart); - if (hotRestart.adopted !== true || typeof hotRestart.adoptedAt !== "string") return null; + if (hotRestart.adopted !== true || typeof hotRestart.adoptedAt !== "string") + return null; return hotRestart; } @@ -6454,7 +7390,9 @@ const defaultSessionCodec: AdapterSessionCodec = { deserialize(raw: unknown) { const asObj = parseObject(raw); if (Object.keys(asObj).length > 0) return asObj; - const sessionId = readNonEmptyString((raw as Record | null)?.sessionId); + const sessionId = readNonEmptyString( + (raw as Record | null)?.sessionId, + ); if (sessionId) return { sessionId }; return null; }, @@ -6472,12 +7410,15 @@ function getAdapterSessionCodec(adapterType: string) { return adapter.sessionCodec ?? defaultSessionCodec; } -export function normalizeSessionParams(params: Record | null | undefined) { +export function normalizeSessionParams( + params: Record | null | undefined, +) { if (!params) return null; return Object.keys(params).length > 0 ? params : null; } -type RunSessionOutcome = "succeeded" | "interrupted" | "failed" | "cancelled" | "timed_out"; +type RunSessionOutcome = + "succeeded" | "interrupted" | "failed" | "cancelled" | "timed_out"; type SkillTestHeartbeatCompletion = { outcome: "failed" | "cancelled"; @@ -6514,7 +7455,8 @@ export function resolveSkillTestRunCompletionForHeartbeatOutcome( } const HERMES_ADAPTER_TYPE = "hermes_local"; -const HERMES_SESSION_ID_REGEX = /^(?:\d{8}_\d{6}_[A-Za-z0-9_-]{4,}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/; +const HERMES_SESSION_ID_REGEX = + /^(?:\d{8}_\d{6}_[A-Za-z0-9_-]{4,}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/; function requiresCanonicalSessionIds(adapterType: string | null | undefined) { return adapterType === HERMES_ADAPTER_TYPE; @@ -6537,7 +7479,9 @@ function normalizeResumeParamsForAdapter( if (!normalized) return null; if (!requiresCanonicalSessionIds(adapterType)) return normalized; const sessionId = readNonEmptyString(normalized.sessionId); - return isCanonicalSessionIdForAdapter(adapterType, sessionId) ? normalized : null; + return isCanonicalSessionIdForAdapter(adapterType, sessionId) + ? normalized + : null; } export function resolveNextSessionState(input: { @@ -6549,7 +7493,14 @@ export function resolveNextSessionState(input: { previousDisplayId: string | null; previousLegacySessionId: string | null; }) { - const { adapterType, codec, adapterResult, previousParams, previousDisplayId, previousLegacySessionId } = input; + const { + adapterType, + codec, + adapterResult, + previousParams, + previousDisplayId, + previousLegacySessionId, + } = input; if (adapterResult.clearSession) { return { @@ -6565,17 +7516,23 @@ export function resolveNextSessionState(input: { const hasExplicitSessionId = adapterResult.sessionId !== undefined; const explicitSessionId = readNonEmptyString(adapterResult.sessionId); const hasExplicitDisplay = adapterResult.sessionDisplayId !== undefined; - const explicitDisplayId = readNonEmptyString(adapterResult.sessionDisplayId); - const shouldUsePrevious = !hasExplicitParams && !hasExplicitSessionId && !hasExplicitDisplay; + const explicitDisplayId = readNonEmptyString( + adapterResult.sessionDisplayId, + ); + const shouldUsePrevious = + !hasExplicitParams && !hasExplicitSessionId && !hasExplicitDisplay; - const candidateParams = - hasExplicitParams - ? explicitParams - : hasExplicitSessionId - ? (explicitSessionId ? { sessionId: explicitSessionId } : null) - : previousParams; + const candidateParams = hasExplicitParams + ? explicitParams + : hasExplicitSessionId + ? explicitSessionId + ? { sessionId: explicitSessionId } + : null + : previousParams; - const serialized = normalizeSessionParams(codec.serialize(normalizeSessionParams(candidateParams) ?? null)); + const serialized = normalizeSessionParams( + codec.serialize(normalizeSessionParams(candidateParams) ?? null), + ); const deserialized = normalizeSessionParams(codec.deserialize(serialized)); const displayId = truncateDisplayId( @@ -6602,12 +7559,20 @@ export function resolveNextSessionState(input: { const previousSerializedParams = normalizeResumeParamsForAdapter( adapterType, - codec.serialize(normalizeResumeParamsForAdapter(adapterType, previousParams)), + codec.serialize( + normalizeResumeParamsForAdapter(adapterType, previousParams), + ), ); - const validPreviousDisplayId = isCanonicalSessionIdForAdapter(adapterType, previousDisplayId) + const validPreviousDisplayId = isCanonicalSessionIdForAdapter( + adapterType, + previousDisplayId, + ) ? previousDisplayId : null; - const validPreviousLegacySessionId = isCanonicalSessionIdForAdapter(adapterType, previousLegacySessionId) + const validPreviousLegacySessionId = isCanonicalSessionIdForAdapter( + adapterType, + previousLegacySessionId, + ) ? previousLegacySessionId : null; const previousState = () => { @@ -6619,7 +7584,10 @@ export function resolveNextSessionState(input: { return { params: previousSerializedParams, displayId, - legacySessionId: readNonEmptyString(previousSerializedParams?.sessionId) ?? displayId ?? validPreviousLegacySessionId, + legacySessionId: + readNonEmptyString(previousSerializedParams?.sessionId) ?? + displayId ?? + validPreviousLegacySessionId, }; }; @@ -6630,11 +7598,17 @@ export function resolveNextSessionState(input: { const explicitParams = adapterResult.sessionParams; const hasExplicitParams = adapterResult.sessionParams !== undefined; const explicitSessionId = readNonEmptyString(adapterResult.sessionId); - const validExplicitSessionId = isCanonicalSessionIdForAdapter(adapterType, explicitSessionId) + const validExplicitSessionId = isCanonicalSessionIdForAdapter( + adapterType, + explicitSessionId, + ) ? explicitSessionId : null; const explicitDisplayId = readNonEmptyString(adapterResult.sessionDisplayId); - const validExplicitDisplayId = isCanonicalSessionIdForAdapter(adapterType, explicitDisplayId) + const validExplicitDisplayId = isCanonicalSessionIdForAdapter( + adapterType, + explicitDisplayId, + ) ? explicitDisplayId : null; const explicitSerializedParams = hasExplicitParams @@ -6661,7 +7635,8 @@ export function resolveNextSessionState(input: { (codec.getDisplayId ? codec.getDisplayId(serialized) : null) ?? explicitCanonicalSessionId, ); - const legacySessionId = readNonEmptyString(serialized?.sessionId) ?? explicitCanonicalSessionId; + const legacySessionId = + readNonEmptyString(serialized?.sessionId) ?? explicitCanonicalSessionId; return { params: serialized, @@ -6670,7 +7645,9 @@ export function resolveNextSessionState(input: { }; } -export type HeartbeatEnvironmentRuntime = ReturnType; +export type HeartbeatEnvironmentRuntime = ReturnType< + typeof environmentRuntimeService +>; export interface HeartbeatServiceOptions { pluginWorkerManager?: PluginWorkerManager; @@ -6681,7 +7658,9 @@ export interface HeartbeatServiceOptions { * the seam here exercises the production reaper, claim, execution, package * session loop, persistence port, and finalizer without spawning a provider. */ - nativeSessionBackendFactory?: (execution: NativeExecutionInput) => NativeSessionBackend; + nativeSessionBackendFactory?: ( + execution: NativeExecutionInput, + ) => NativeSessionBackend; } type WorkspaceReadyCommentWriter = { @@ -6726,8 +7705,14 @@ function isTruthyRuntimeEnvValue(value: string | undefined) { export function resolveHeartbeatSchedulingSuppression( env: Record = process.env, overrides: { allowWorktreeRunExecution?: boolean } = {}, -): { suppressed: boolean; reason: "worktree_instance" | "database_restore_in_progress" | null } { - if (isTruthyRuntimeEnvValue(env.PAPERCLIP_IN_WORKTREE) && !overrides.allowWorktreeRunExecution) { +): { + suppressed: boolean; + reason: "worktree_instance" | "database_restore_in_progress" | null; +} { + if ( + isTruthyRuntimeEnvValue(env.PAPERCLIP_IN_WORKTREE) && + !overrides.allowWorktreeRunExecution + ) { return { suppressed: true, reason: "worktree_instance" }; } if ( @@ -6739,20 +7724,29 @@ export function resolveHeartbeatSchedulingSuppression( return { suppressed: false, reason: null }; } -export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) { +export function heartbeatService( + db: Db, + options: HeartbeatServiceOptions = {}, +) { const instanceSettings = instanceSettingsService(db); const getCurrentUserRedactionOptions = async () => ({ enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs, }); const runtimeEnv = options.runtimeEnv ?? process.env; - const inWorktreeRuntime = isTruthyRuntimeEnvValue(runtimeEnv.PAPERCLIP_IN_WORKTREE); + const inWorktreeRuntime = isTruthyRuntimeEnvValue( + runtimeEnv.PAPERCLIP_IN_WORKTREE, + ); // Preview worktree instances suppress the run engine by default. Users can lift // that per-worktree via the `enableWorktreeRunExecution` experimental setting // (worktree instances have their own isolated DB, so it can't affect the parent). // Only worktree runtimes ever read the setting; a short TTL keeps the hot-path // suppression checks off the DB, and a read failure falls back to prior/default // (fail closed to suppression). - let cachedWorktreeRunExecutionOverride: { allowed: boolean; cutoff: Date | null; at: number } = { + let cachedWorktreeRunExecutionOverride: { + allowed: boolean; + cutoff: Date | null; + at: number; + } = { allowed: false, cutoff: null, at: 0, @@ -6761,7 +7755,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const resolveWorktreeRunExecutionOverride = async () => { if (!inWorktreeRuntime) return { allowed: false, cutoff: null }; const now = Date.now(); - if (now - cachedWorktreeRunExecutionOverride.at < WORKTREE_RUN_EXECUTION_OVERRIDE_TTL_MS) { + if ( + now - cachedWorktreeRunExecutionOverride.at < + WORKTREE_RUN_EXECUTION_OVERRIDE_TTL_MS + ) { return cachedWorktreeRunExecutionOverride; } try { @@ -6771,7 +7768,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ); const cutoff = activation.armed ? new Date(activation.cutoff) : null; cachedWorktreeRunExecutionOverride = { - allowed: Boolean(activation.armed && cutoff && !Number.isNaN(cutoff.getTime())), + allowed: Boolean( + activation.armed && cutoff && !Number.isNaN(cutoff.getTime()), + ), cutoff: cutoff && !Number.isNaN(cutoff.getTime()) ? cutoff : null, at: now, }; @@ -6793,15 +7792,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; const runLogStore = getRunLogStore(); + const traceStore = providerTraceStore(db); const secretsSvc = secretService(db); const companySkills = companySkillService(db); const issuesSvc = issueService(db); const treeControlSvc = issueTreeControlService(db); const executionWorkspacesSvc = executionWorkspaceService(db); const environmentsSvc = environmentService(db); - const environmentRuntime = options.environmentRuntime ?? environmentRuntimeService(db, { - pluginWorkerManager: options.pluginWorkerManager, - }); + const environmentRuntime = + options.environmentRuntime ?? + environmentRuntimeService(db, { + pluginWorkerManager: options.pluginWorkerManager, + }); const envOrchestrator = environmentRunOrchestrator(db, { pluginWorkerManager: options.pluginWorkerManager, environmentRuntime, @@ -6820,8 +7822,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) function isPlanApprovalConfirmationPayload(payload: unknown) { const target = parseObject(parseObject(payload).target); - return readNonEmptyString(target.type) === "issue_document" && - readNonEmptyString(target.key) === "plan"; + return ( + readNonEmptyString(target.type) === "issue_document" && + readNonEmptyString(target.key) === "plan" + ); } async function getAcceptedPlanApprovalInteractionForRun( @@ -6851,11 +7855,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .then((rows) => rows[0] ?? null); if (!interaction) return null; - if (interaction.kind !== "request_confirmation" || interaction.status !== "accepted") return null; - return isPlanApprovalConfirmationPayload(interaction.payload) ? interaction : null; + if ( + interaction.kind !== "request_confirmation" || + interaction.status !== "accepted" + ) + return null; + return isPlanApprovalConfirmationPayload(interaction.payload) + ? interaction + : null; } - function planApprovalResumeFailureErrorCode(run: typeof heartbeatRuns.$inferSelect) { + function planApprovalResumeFailureErrorCode( + run: typeof heartbeatRuns.$inferSelect, + ) { return readNonEmptyString(run.errorCode) ?? "unknown_error"; } @@ -6893,7 +7905,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } async function updatePlanApprovalInteractionResumeFailure(input: { - interaction: NonNullable>>; + interaction: NonNullable< + Awaited> + >; failure: NonNullable; }) { const result = parseObject(input.interaction.result); @@ -6934,10 +7948,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .limit(1) .then((rows) => rows[0] ?? null); if (existing) return null; - return issuesSvc.addComment(input.issueId, input.body, { runId: input.run.id }, { authorType: "system" }); + return issuesSvc.addComment( + input.issueId, + input.body, + { runId: input.run.id }, + { authorType: "system" }, + ); } - async function getActiveRecoveryActionId(companyId: string, sourceIssueId: string) { + async function getActiveRecoveryActionId( + companyId: string, + sourceIssueId: string, + ) { return db .select({ id: issueRecoveryActions.id }) .from(issueRecoveryActions) @@ -6960,7 +7982,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) attempt: number; maxAttempts: number; }) { - const interaction = await getAcceptedPlanApprovalInteractionForRun(input.run, input.issueId); + const interaction = await getAcceptedPlanApprovalInteractionForRun( + input.run, + input.issueId, + ); if (!interaction || !input.issueId) return null; const body = buildPlanApprovalResumeFailureComment({ @@ -6993,16 +8018,29 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) attempt: number; maxAttempts: number; }) { - const interaction = await getAcceptedPlanApprovalInteractionForRun(input.run, input.issueId); + const interaction = await getAcceptedPlanApprovalInteractionForRun( + input.run, + input.issueId, + ); if (!interaction || !input.issueId) return null; const issue = await db .select() .from(issues) - .where(and(eq(issues.companyId, input.run.companyId), eq(issues.id, input.issueId))) + .where( + and( + eq(issues.companyId, input.run.companyId), + eq(issues.id, input.issueId), + ), + ) .then((rows) => rows[0] ?? null); if (!issue) return null; - if (issue.status !== "todo" && issue.status !== "in_progress" && issue.status !== "in_review") return null; + if ( + issue.status !== "todo" && + issue.status !== "in_progress" && + issue.status !== "in_review" + ) + return null; const body = buildPlanApprovalResumeFailureComment({ run: input.run, @@ -7022,7 +8060,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) body, }); - const recoveryActionId = await getActiveRecoveryActionId(issue.companyId, issue.id); + const recoveryActionId = await getActiveRecoveryActionId( + issue.companyId, + issue.id, + ); await updatePlanApprovalInteractionResumeFailure({ interaction, failure: buildPlanApprovalResumeFailureResult({ @@ -7047,7 +8088,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) outcome: RunSessionOutcome; error: string | null; }) { - const completion = resolveSkillTestRunCompletionForHeartbeatOutcome(input.outcome, input.error); + const completion = resolveSkillTestRunCompletionForHeartbeatOutcome( + input.outcome, + input.error, + ); if (!completion || !input.issueId) return null; let isSkillTestIssue = input.issueWorkMode === "skill_test"; @@ -7058,9 +8102,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) harnessKind: issues.harnessKind, }) .from(issues) - .where(and(eq(issues.companyId, input.run.companyId), eq(issues.id, input.issueId))) + .where( + and( + eq(issues.companyId, input.run.companyId), + eq(issues.id, input.issueId), + ), + ) .then((rows) => rows[0] ?? null); - isSkillTestIssue = issueRow?.workMode === "skill_test" || issueRow?.harnessKind === "skill_test"; + isSkillTestIssue = + issueRow?.workMode === "skill_test" || + issueRow?.harnessKind === "skill_test"; } if (!isSkillTestIssue) return null; @@ -7070,12 +8121,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) status: companySkillTestRuns.status, }) .from(companySkillTestRuns) - .where(and( - eq(companySkillTestRuns.companyId, input.run.companyId), - eq(companySkillTestRuns.issueId, input.issueId), - )) + .where( + and( + eq(companySkillTestRuns.companyId, input.run.companyId), + eq(companySkillTestRuns.issueId, input.issueId), + ), + ) .then((rows) => rows[0] ?? null); - if (!existingRun || ["succeeded", "failed", "cancelled"].includes(existingRun.status)) return null; + if ( + !existingRun || + ["succeeded", "failed", "cancelled"].includes(existingRun.status) + ) + return null; const completedRun = await companySkills.completeTestRunForIssue({ companyId: input.run.companyId, @@ -7114,19 +8171,28 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) status: string | null | undefined; failureReason?: string | null; }) { - const releaseResult = await envOrchestrator.releaseForRun({ - heartbeatRunId: input.runId, - companyId: input.companyId, - agentId: input.agentId, - status: leaseReleaseStatusForRunStatus(input.status), - failureReason: input.failureReason ?? undefined, - }).catch((err) => { - logger.warn({ err, runId: input.runId }, "failed to release environment leases for heartbeat run"); - return null; - }); + const releaseResult = await envOrchestrator + .releaseForRun({ + heartbeatRunId: input.runId, + companyId: input.companyId, + agentId: input.agentId, + status: leaseReleaseStatusForRunStatus(input.status), + failureReason: input.failureReason ?? undefined, + }) + .catch((err) => { + logger.warn( + { err, runId: input.runId }, + "failed to release environment leases for heartbeat run", + ); + return null; + }); for (const releaseError of releaseResult?.errors ?? []) { logger.warn( - { err: releaseError.error, leaseId: releaseError.leaseId, runId: input.runId }, + { + err: releaseError.error, + leaseId: releaseError.leaseId, + runId: input.runId, + }, "failed to release environment lease for heartbeat run", ); } @@ -7135,16 +8201,25 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) async function hasUnsafeTextProjectionDatabase() { if (!unsafeTextProjectionPromise) { unsafeTextProjectionPromise = db - .execute(sql`select current_setting('server_encoding') as server_encoding`) + .execute( + sql`select current_setting('server_encoding') as server_encoding`, + ) .then((rows) => { const first = Array.isArray(rows) ? rows[0] : null; - const serverEncoding = typeof first === "object" && first !== null - ? (first as Record).server_encoding - : null; - return typeof serverEncoding === "string" && serverEncoding.toUpperCase() === "SQL_ASCII"; + const serverEncoding = + typeof first === "object" && first !== null + ? (first as Record).server_encoding + : null; + return ( + typeof serverEncoding === "string" && + serverEncoding.toUpperCase() === "SQL_ASCII" + ); }) .catch((err) => { - logger.warn({ err }, "failed to inspect database server encoding; using conservative heartbeat result projection"); + logger.warn( + { err }, + "failed to inspect database server encoding; using conservative heartbeat result projection", + ); return true; }); } @@ -7159,11 +8234,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .then((rows) => rows[0] ?? null); } - async function getAgentInvokability(agent: typeof agents.$inferSelect | null | undefined) { + async function getAgentInvokability( + agent: typeof agents.$inferSelect | null | undefined, + ) { return evaluateAgentInvokabilityFromDb(db, agent); } - function toAgentOrgRow(agent: Pick): AgentOrgRow { + function toAgentOrgRow( + agent: Pick< + typeof agents.$inferSelect, + "id" | "companyId" | "name" | "reportsTo" | "status" + >, + ): AgentOrgRow { return { id: agent.id, companyId: agent.companyId, @@ -7173,7 +8255,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; } - async function listCompanyAgentOrgRows(companyId: string): Promise { + async function listCompanyAgentOrgRows( + companyId: string, + ): Promise { return db .select({ id: agents.id, @@ -7199,8 +8283,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return byCompany; } - async function getRun(runId: string, opts?: { unsafeFullResultJson?: boolean }) { - const safeForLegacyEncoding = !opts?.unsafeFullResultJson && await hasUnsafeTextProjectionDatabase(); + async function getRun( + runId: string, + opts?: { unsafeFullResultJson?: boolean }, + ) { + const safeForLegacyEncoding = + !opts?.unsafeFullResultJson && (await hasUnsafeTextProjectionDatabase()); return db .select( opts?.unsafeFullResultJson @@ -7215,7 +8303,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } async function recordCurrentHeartbeatRunRuntimeProgress( - run: Pick, + run: Pick< + typeof heartbeatRuns.$inferSelect, + "id" | "companyId" | "agentId" | "status" | "contextSnapshot" + >, update: RuntimeStatusUpdate, issueId: string | null, ) { @@ -7293,21 +8384,29 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eq(companySkillVersions.companyId, companySkillTestRuns.companyId), ), ) - .where(and(eq(companySkillTestRuns.companyId, companyId), eq(companySkillTestRuns.issueId, issueId))) + .where( + and( + eq(companySkillTestRuns.companyId, companyId), + eq(companySkillTestRuns.issueId, issueId), + ), + ) .then((rows) => rows[0] ?? null); if (!row) return null; const fileInventory = Array.isArray(row.fileInventory) ? row.fileInventory.flatMap((entry) => { - if (!entry || typeof entry !== "object" || Array.isArray(entry)) return []; - const record = entry as unknown as Record; - const path = typeof record.path === "string" ? record.path : ""; - if (!path) return []; - return [{ - path, - kind: typeof record.kind === "string" ? record.kind : "other", - content: typeof record.content === "string" ? record.content : "", - }]; - }) + if (!entry || typeof entry !== "object" || Array.isArray(entry)) + return []; + const record = entry as unknown as Record; + const path = typeof record.path === "string" ? record.path : ""; + if (!path) return []; + return [ + { + path, + kind: typeof record.kind === "string" ? record.kind : "other", + content: typeof record.content === "string" ? record.content : "", + }, + ]; + }) : []; return { testRunId: row.testRunId, @@ -7325,7 +8424,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) companyId: string, issueContext: Awaited> | null, ) { - if (!issueContext || issueContext.originKind !== "routine_execution" || !issueContext.originId) { + if ( + !issueContext || + issueContext.originKind !== "routine_execution" || + !issueContext.originId + ) { return { routineId: null, env: null, responsibleUserId: null }; } @@ -7361,25 +8464,38 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ), ) .then((rows) => rows[0] ?? null); - const snapshot = revision?.snapshot as RoutineRevisionSnapshotV1 | undefined; + const snapshot = revision?.snapshot as + RoutineRevisionSnapshotV1 | undefined; if (snapshot?.version === 1) { return { routineId: issueContext.originId, env: snapshot.routine.env ?? null, - responsibleUserId: revision?.responsibleUserId ?? snapshot.routine.responsibleUserId ?? null, + responsibleUserId: + revision?.responsibleUserId ?? + snapshot.routine.responsibleUserId ?? + null, }; } } const routine = await db - .select({ env: routines.env, responsibleUserId: routines.responsibleUserId }) + .select({ + env: routines.env, + responsibleUserId: routines.responsibleUserId, + }) .from(routines) - .where(and(eq(routines.id, issueContext.originId), eq(routines.companyId, companyId))) + .where( + and( + eq(routines.id, issueContext.originId), + eq(routines.companyId, companyId), + ), + ) .then((rows) => rows[0] ?? null); return { routineId: issueContext.originId, env: routine?.env ?? null, - responsibleUserId: routineRun?.responsibleUserId ?? routine?.responsibleUserId ?? null, + responsibleUserId: + routineRun?.responsibleUserId ?? routine?.responsibleUserId ?? null, }; } @@ -7389,7 +8505,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .from(companies) .where(eq(companies.id, companyId)) .then((rows) => rows[0] ?? null); - const explicitDefault = readNonEmptyString(company?.defaultResponsibleUserId); + const explicitDefault = readNonEmptyString( + company?.defaultResponsibleUserId, + ); if (explicitDefault) return explicitDefault; const owner = await db @@ -7424,7 +8542,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return firstUser?.userId ?? null; } - async function resolveParentIssueResponsibleUserId(companyId: string, parentId: string | null | undefined) { + async function resolveParentIssueResponsibleUserId( + companyId: string, + parentId: string | null | undefined, + ) { if (!parentId) return null; const parent = await db .select({ @@ -7445,7 +8566,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }) { if (input.requestedByActorType !== "user") return false; const wakeReason = readNonEmptyString(input.contextSnapshot.wakeReason); - if (wakeReason && ISSUE_RESPONSIBLE_USER_WAKE_REASONS.has(wakeReason)) return false; + if (wakeReason && ISSUE_RESPONSIBLE_USER_WAKE_REASONS.has(wakeReason)) + return false; return input.source === "on_demand" || input.triggerDetail === "manual"; } @@ -7453,25 +8575,37 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) companyId: string; contextSnapshot: Record; issueContext: Awaited> | null; - routineEnvContext: Awaited>; + routineEnvContext: Awaited< + ReturnType + >; requestedByActorType?: "user" | "agent" | "system" | null; requestedByActorId?: string | null; source?: WakeupOptions["source"] | null; triggerDetail?: WakeupOptions["triggerDetail"] | null; existingRunResponsibleUserId?: string | null; }) { - const contextResponsibleUserId = readNonEmptyString(input.contextSnapshot.responsibleUserId); - const requestedUserId = input.requestedByActorType === "user" - ? readNonEmptyString(input.requestedByActorId) - : null; + const contextResponsibleUserId = readNonEmptyString( + input.contextSnapshot.responsibleUserId, + ); + const requestedUserId = + input.requestedByActorType === "user" + ? readNonEmptyString(input.requestedByActorId) + : null; if (contextResponsibleUserId) return contextResponsibleUserId; - if (input.existingRunResponsibleUserId) return input.existingRunResponsibleUserId; - if (input.routineEnvContext.responsibleUserId) return input.routineEnvContext.responsibleUserId; + if (input.existingRunResponsibleUserId) + return input.existingRunResponsibleUserId; + if (input.routineEnvContext.responsibleUserId) + return input.routineEnvContext.responsibleUserId; if (isManualUserRun(input) && requestedUserId) return requestedUserId; - if (input.issueContext?.responsibleUserId) return input.issueContext.responsibleUserId; - const parentResponsibleUserId = await resolveParentIssueResponsibleUserId(input.companyId, input.issueContext?.parentId); + if (input.issueContext?.responsibleUserId) + return input.issueContext.responsibleUserId; + const parentResponsibleUserId = await resolveParentIssueResponsibleUserId( + input.companyId, + input.issueContext?.parentId, + ); if (parentResponsibleUserId) return parentResponsibleUserId; - if (input.issueContext) return resolveCompanyDefaultResponsibleUserId(input.companyId); + if (input.issueContext) + return resolveCompanyDefaultResponsibleUserId(input.companyId); if (requestedUserId) return requestedUserId; return resolveCompanyDefaultResponsibleUserId(input.companyId); } @@ -7480,7 +8614,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) run: typeof heartbeatRuns.$inferSelect; contextSnapshot: Record; issueContext: Awaited> | null; - routineEnvContext: Awaited>; + routineEnvContext: Awaited< + ReturnType + >; }) { const responsibleUserId = await resolveResponsibleUserIdForRunSeed({ companyId: input.run.companyId, @@ -7492,16 +8628,20 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: input.run.triggerDetail as WakeupOptions["triggerDetail"], }); if (!responsibleUserId) { - throw new HttpError(422, "Unable to resolve responsible user for heartbeat run dispatch", { - code: "responsible_user_unresolved", - runId: input.run.id, - agentId: input.run.agentId, - companyId: input.run.companyId, - issueId: input.issueContext?.id ?? null, - invocationSource: input.run.invocationSource, - triggerDetail: input.run.triggerDetail, - wakeReason: readNonEmptyString(input.contextSnapshot.wakeReason), - }); + throw new HttpError( + 422, + "Unable to resolve responsible user for heartbeat run dispatch", + { + code: "responsible_user_unresolved", + runId: input.run.id, + agentId: input.run.agentId, + companyId: input.run.companyId, + issueId: input.issueContext?.id ?? null, + invocationSource: input.run.invocationSource, + triggerDetail: input.run.triggerDetail, + wakeReason: readNonEmptyString(input.contextSnapshot.wakeReason), + }, + ); } return responsibleUserId; } @@ -7510,13 +8650,20 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) run: typeof heartbeatRuns.$inferSelect, contextSnapshot: Record, ) { - const issueId = readNonEmptyString(contextSnapshot.issueId) ?? readNonEmptyString(contextSnapshot.taskId); - const issueContext = issueId ? await getIssueExecutionContext(run.companyId, issueId) : null; + const issueId = + readNonEmptyString(contextSnapshot.issueId) ?? + readNonEmptyString(contextSnapshot.taskId); + const issueContext = issueId + ? await getIssueExecutionContext(run.companyId, issueId) + : null; return resolveResponsibleUserIdForRun({ run, contextSnapshot, issueContext, - routineEnvContext: await getRoutineEnvForExecutionIssue(run.companyId, issueContext), + routineEnvContext: await getRoutineEnvForExecutionIssue( + run.companyId, + issueContext, + ), }); } @@ -7528,7 +8675,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .then((rows) => rows[0] ?? null); } - async function getLatestAgentConfigRevision(companyId: string, agentId: string) { + async function getLatestAgentConfigRevision( + companyId: string, + agentId: string, + ) { return db .select({ id: agentConfigRevisions.id, @@ -7536,8 +8686,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) createdAt: agentConfigRevisions.createdAt, }) .from(agentConfigRevisions) - .where(and(eq(agentConfigRevisions.companyId, companyId), eq(agentConfigRevisions.agentId, agentId))) - .orderBy(desc(agentConfigRevisions.createdAt), desc(agentConfigRevisions.id)) + .where( + and( + eq(agentConfigRevisions.companyId, companyId), + eq(agentConfigRevisions.agentId, agentId), + ), + ) + .orderBy( + desc(agentConfigRevisions.createdAt), + desc(agentConfigRevisions.id), + ) .limit(1) .then((rows) => rows[0] ?? null); } @@ -7681,7 +8839,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; } - function formatIssueIdentifierLink(identifier: string | null, fallback: string) { + function formatIssueIdentifierLink( + identifier: string | null, + fallback: string, + ) { if (!identifier) return fallback; const prefix = identifier.split("-")[0]; if (!prefix || !/^[A-Z][A-Z0-9]*-\d+$/.test(identifier)) return identifier; @@ -7694,7 +8855,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) recoveryPolicy: IssueExecutionMonitorRecoveryPolicy; nextAttemptCount: number; }) { - const label = formatIssueIdentifierLink(input.issue.identifier, input.issue.id); + const label = formatIssueIdentifierLink( + input.issue.identifier, + input.issue.id, + ); const reason = input.clearReason === "timeout_exceeded" ? "its timeout was reached" @@ -7709,7 +8873,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ].join("\n"); } - async function findOpenIssueMonitorRecoveryIssue(claimed: IssueMonitorDispatchRow) { + async function findOpenIssueMonitorRecoveryIssue( + claimed: IssueMonitorDispatchRow, + ) { return db .select() .from(issues) @@ -7740,15 +8906,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) runId: string | null; activitySource: "manual" | "scheduled"; }) { - const reviewPathLost = input.claimed.status === "in_review" - && (await issuesSvc + const reviewPathLost = + input.claimed.status === "in_review" && + (await issuesSvc .listReviewAttention(input.claimed.companyId, [input.claimed]) - .then((attention) => attention.get(input.claimed.id)?.state === "stalled")); + .then( + (attention) => attention.get(input.claimed.id)?.state === "stalled", + )); const reviewPathContext = reviewPathLost ? { reviewPathLost: true, - reviewPathConsumedRef: - `monitor:${input.claimed.id}:${input.clearReason}:${input.scheduledAtIso}`, + reviewPathConsumedRef: `monitor:${input.claimed.id}:${input.clearReason}:${input.scheduledAtIso}`, reviewPathInstruction: REVIEW_PATH_RECOVERY_INSTRUCTION, } : null; @@ -7763,7 +8931,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); if (input.recoveryPolicy === "create_recovery_issue") { - let recoveryIssue = await findOpenIssueMonitorRecoveryIssue(input.claimed); + let recoveryIssue = await findOpenIssueMonitorRecoveryIssue( + input.claimed, + ); if (!recoveryIssue) { recoveryIssue = await issuesSvc.create(input.claimed.companyId, { title: `Recover external-service monitor for ${input.claimed.identifier ?? input.claimed.title}`, @@ -7779,7 +8949,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) projectId: input.claimed.projectId, goalId: input.claimed.goalId, assigneeAgentId: input.claimed.assigneeAgentId, - assigneeAdapterOverrides: recoveryAssigneeAdapterOverrides("status_only"), + assigneeAdapterOverrides: + recoveryAssigneeAdapterOverrides("status_only"), originKind: RECOVERY_ORIGIN_KINDS.strandedIssueRecovery, originId: input.claimed.id, originFingerprint: `issue_monitor:${input.clearReason}`, @@ -7793,15 +8964,21 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: "system", reason: "issue_monitor_recovery_issue", idempotencyKey: `issue-monitor-recovery-issue:${input.claimed.id}:${input.clearReason}:${input.scheduledAtIso}`, - payload: withRecoveryModelProfileHint({ issueId: recoveryIssue.id, sourceIssueId: input.claimed.id }, "status_only"), + payload: withRecoveryModelProfileHint( + { issueId: recoveryIssue.id, sourceIssueId: input.claimed.id }, + "status_only", + ), requestedByActorType: input.actorType, requestedByActorId: input.actorId, - contextSnapshot: withRecoveryModelProfileHint({ - issueId: recoveryIssue.id, - sourceIssueId: input.claimed.id, - source: "issue.monitor.recovery_issue", - wakeReason: "issue_monitor_recovery_issue", - }, "status_only"), + contextSnapshot: withRecoveryModelProfileHint( + { + issueId: recoveryIssue.id, + sourceIssueId: input.claimed.id, + source: "issue.monitor.recovery_issue", + wakeReason: "issue_monitor_recovery_issue", + }, + "status_only", + ), }); } @@ -7854,30 +9031,36 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: "system", reason: "issue_monitor_recovery", idempotencyKey: `issue-monitor-recovery:${input.claimed.id}:${input.clearReason}:${input.scheduledAtIso}`, - payload: withRecoveryModelProfileHint({ - issueId: input.claimed.id, - monitorAttemptCount: input.nextAttemptCount, - monitorNotes: input.claimed.monitorNotes ?? null, - clearReason: input.clearReason, - serviceName: input.monitor?.serviceName ?? null, - timeoutAt: input.monitor?.timeoutAt ?? null, - maxAttempts: input.monitor?.maxAttempts ?? null, - ...(reviewPathContext ?? {}), - }, "status_only"), + payload: withRecoveryModelProfileHint( + { + issueId: input.claimed.id, + monitorAttemptCount: input.nextAttemptCount, + monitorNotes: input.claimed.monitorNotes ?? null, + clearReason: input.clearReason, + serviceName: input.monitor?.serviceName ?? null, + timeoutAt: input.monitor?.timeoutAt ?? null, + maxAttempts: input.monitor?.maxAttempts ?? null, + ...(reviewPathContext ?? {}), + }, + "status_only", + ), requestedByActorType: input.actorType, requestedByActorId: input.actorId, - contextSnapshot: withRecoveryModelProfileHint({ - issueId: input.claimed.id, - source: "issue.monitor.recovery", - wakeReason: "issue_monitor_recovery", - monitorAttemptCount: input.nextAttemptCount, - monitorNotes: input.claimed.monitorNotes ?? null, - clearReason: input.clearReason, - serviceName: input.monitor?.serviceName ?? null, - timeoutAt: input.monitor?.timeoutAt ?? null, - maxAttempts: input.monitor?.maxAttempts ?? null, - ...(reviewPathContext ?? {}), - }, "status_only"), + contextSnapshot: withRecoveryModelProfileHint( + { + issueId: input.claimed.id, + source: "issue.monitor.recovery", + wakeReason: "issue_monitor_recovery", + monitorAttemptCount: input.nextAttemptCount, + monitorNotes: input.claimed.monitorNotes ?? null, + clearReason: input.clearReason, + serviceName: input.monitor?.serviceName ?? null, + timeoutAt: input.monitor?.timeoutAt ?? null, + maxAttempts: input.monitor?.maxAttempts ?? null, + ...(reviewPathContext ?? {}), + }, + "status_only", + ), }); await logActivity(db, { @@ -7979,9 +9162,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const scheduledAtIso = claimed.monitorNextCheckAt.toISOString(); const nextAttemptCount = (claimed.monitorAttemptCount ?? 0) + 1; - const policy = normalizeIssueExecutionPolicy(claimed.executionPolicy ?? null); + const policy = normalizeIssueExecutionPolicy( + claimed.executionPolicy ?? null, + ); const monitor = policy?.monitor ?? null; - const clearReason = issueMonitorLimitClearReason({ monitor, nextAttemptCount, now: input.now }); + const clearReason = issueMonitorLimitClearReason({ + monitor, + nextAttemptCount, + now: input.now, + }); const recoveryPolicy = monitorRecoveryPolicy(monitor); const monitorMetadata = { serviceName: monitor?.serviceName ?? null, @@ -7989,16 +9178,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) maxAttempts: monitor?.maxAttempts ?? null, recoveryPolicy: monitor?.recoveryPolicy ?? null, }; - const executionState = claimed.status === "in_review" - ? parseIssueExecutionState(claimed.executionState) - : null; - const currentParticipant = executionState?.status === "pending" - ? executionState.currentParticipant - : null; - const reviewParticipantAgentId = currentParticipant?.type === "agent" - ? currentParticipant.agentId - : null; - const isProviderQuotaReviewMonitor = monitor?.serviceName === PROVIDER_QUOTA_MONITOR_SERVICE_NAME && + const executionState = + claimed.status === "in_review" + ? parseIssueExecutionState(claimed.executionState) + : null; + const currentParticipant = + executionState?.status === "pending" + ? executionState.currentParticipant + : null; + const reviewParticipantAgentId = + currentParticipant?.type === "agent" ? currentParticipant.agentId : null; + const isProviderQuotaReviewMonitor = + monitor?.serviceName === PROVIDER_QUOTA_MONITOR_SERVICE_NAME && Boolean(reviewParticipantAgentId); const targetAgentId = isProviderQuotaReviewMonitor ? reviewParticipantAgentId @@ -8056,7 +9247,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) requestedByActorId: input.actorId, contextSnapshot: { issueId: claimed.id, - source: isProviderQuotaReviewMonitor ? "issue.execution_review_recovery" : "issue.monitor", + source: isProviderQuotaReviewMonitor + ? "issue.execution_review_recovery" + : "issue.monitor", wakeReason, nextCheckAt: scheduledAtIso, monitorAttemptCount: nextAttemptCount, @@ -8159,17 +9352,21 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } - async function triggerIssueMonitor(issueId: string, input?: { - now?: Date; - actorType?: "user" | "agent" | "system"; - actorId?: string | null; - agentId?: string | null; - runId?: string | null; - wakeReason?: string; - }) { + async function triggerIssueMonitor( + issueId: string, + input?: { + now?: Date; + actorType?: "user" | "agent" | "system"; + actorId?: string | null; + agentId?: string | null; + runId?: string | null; + wakeReason?: string; + }, + ) { const now = input?.now ?? new Date(); const actorType = input?.actorType ?? "system"; - const actorId = input?.actorId ?? (actorType === "system" ? "heartbeat_scheduler" : null); + const actorId = + input?.actorId ?? (actorType === "system" ? "heartbeat_scheduler" : null); if (!actorId) { throw conflict("Issue monitor trigger requires an actor"); } @@ -8190,7 +9387,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) throw conflict("Issue monitor requires an agent assignee"); } if (!["in_progress", "in_review"].includes(issue.status)) { - throw conflict("Issue monitor can only run while the issue is in progress or in review"); + throw conflict( + "Issue monitor can only run while the issue is in progress or in review", + ); } const staleClaimThreshold = new Date(now.getTime() - 5 * 60 * 1000); @@ -8324,7 +9523,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) createdAt: heartbeatRuns.createdAt, }) .from(heartbeatRuns) - .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.sessionIdAfter, sessionId))) + .where( + and( + eq(heartbeatRuns.agentId, agentId), + eq(heartbeatRuns.sessionIdAfter, sessionId), + ), + ) .orderBy(asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id)) .limit(1) .then((rows) => rows[0] ?? null); @@ -8349,7 +9553,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; } - const previousRun = await getLatestRunForSession(agentId, sessionId, { excludeRunId: runId }); + const previousRun = await getLatestRunForSession(agentId, sessionId, { + excludeRunId: runId, + }); const previousRawUsage = readRawUsageTotals(previousRun?.usageJson); return { normalizedUsage: deriveNormalizedUsageDelta(rawUsage, previousRawUsage), @@ -8384,7 +9590,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; } - const fetchLimit = Math.max(policy.maxSessionRuns > 0 ? policy.maxSessionRuns + 1 : 0, 4); + const fetchLimit = Math.max( + policy.maxSessionRuns > 0 ? policy.maxSessionRuns + 1 : 0, + 4, + ); const runs = await db .select({ id: heartbeatRuns.id, @@ -8394,7 +9603,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ...heartbeatRunListResultColumns, }) .from(heartbeatRuns) - .where(and(eq(heartbeatRuns.agentId, agent.id), eq(heartbeatRuns.sessionIdAfter, sessionId))) + .where( + and( + eq(heartbeatRuns.agentId, agent.id), + eq(heartbeatRuns.sessionIdAfter, sessionId), + ), + ) .orderBy(desc(heartbeatRuns.createdAt)) .limit(fetchLimit); @@ -8411,13 +9625,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const oldestRun = policy.maxSessionAgeHours > 0 ? await getOldestRunForSession(agent.id, sessionId) - : runs[runs.length - 1] ?? latestRun; + : (runs[runs.length - 1] ?? latestRun); const latestRawUsage = readRawUsageTotals(latestRun?.usageJson); const sessionAgeHours = latestRun && oldestRun ? Math.max( 0, - (new Date(latestRun.createdAt).getTime() - new Date(oldestRun.createdAt).getTime()) / (1000 * 60 * 60), + (new Date(latestRun.createdAt).getTime() - + new Date(oldestRun.createdAt).getTime()) / + (1000 * 60 * 60), ) : 0; @@ -8432,7 +9648,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) reason = `session raw input reached ${formatCount(latestRawUsage.inputTokens)} tokens ` + `(threshold ${formatCount(policy.maxRawInputTokens)})`; - } else if (policy.maxSessionAgeHours > 0 && sessionAgeHours >= policy.maxSessionAgeHours) { + } else if ( + policy.maxSessionAgeHours > 0 && + sessionAgeHours >= policy.maxSessionAgeHours + ) { reason = `session age reached ${Math.floor(sessionAgeHours)} hours`; } @@ -8508,7 +9727,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return runtimeForRun?.sessionId ?? null; } - async function hasResolvableSessionWorkspaceCwd(sessionParams: Record | null | undefined) { + async function hasResolvableSessionWorkspaceCwd( + sessionParams: Record | null | undefined, + ) { const cwd = readNonEmptyString(sessionParams?.cwd); if (!cwd || isUnsafeSessionWorkspaceCwd(cwd)) return false; return fs @@ -8521,9 +9742,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) agent: typeof agents.$inferSelect; contextSnapshot: Record; taskKey: string | null; - explicitResumeSession: Awaited> | null; + explicitResumeSession: Awaited< + ReturnType + > | null; }) { - if (await hasResolvableSessionWorkspaceCwd(input.explicitResumeSession?.sessionParams)) return true; + if ( + await hasResolvableSessionWorkspaceCwd( + input.explicitResumeSession?.sessionParams, + ) + ) + return true; if (shouldResetTaskSessionForWake(input.contextSnapshot)) return false; if (!input.taskKey) return false; @@ -8571,19 +9799,27 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const resumeContext = parseObject(resumeRun.contextSnapshot); const resumeTaskKey = deriveTaskKey(resumeContext, null) ?? taskKey; const resumeTaskSession = resumeTaskKey - ? await getTaskSession(agent.companyId, agent.id, agent.adapterType, resumeTaskKey) + ? await getTaskSession( + agent.companyId, + agent.id, + agent.adapterType, + resumeTaskKey, + ) : null; const sessionCodec = getAdapterSessionCodec(agent.adapterType); const resumeRunResult = parseObject(resumeRun.resultJson); const resumeRunSessionId = requiresCanonicalSessionIds(agent.adapterType) - ? readNonEmptyString(resumeRunResult.sessionId) ?? readNonEmptyString(resumeRunResult.session_id) + ? (readNonEmptyString(resumeRunResult.sessionId) ?? + readNonEmptyString(resumeRunResult.session_id)) : null; const sessionOverride = buildExplicitResumeSessionOverride({ adapterType: agent.adapterType, resumeFromRunId, resumeRunSessionIdBefore: resumeRun.sessionIdBefore, resumeRunSessionIdAfter: resumeRun.sessionIdAfter, - resumeRunSessionParams: resumeRunSessionId ? { sessionId: resumeRunSessionId } : null, + resumeRunSessionParams: resumeRunSessionId + ? { sessionId: resumeRunSessionId } + : null, taskSession: resumeTaskSession, sessionCodec, }); @@ -8593,7 +9829,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) resumeFromRunId, taskKey: resumeTaskKey, issueId: readNonEmptyString(resumeContext.issueId), - taskId: readNonEmptyString(resumeContext.taskId) ?? readNonEmptyString(resumeContext.issueId), + taskId: + readNonEmptyString(resumeContext.taskId) ?? + readNonEmptyString(resumeContext.issueId), sessionDisplayId: sessionOverride.sessionDisplayId, sessionParams: sessionOverride.sessionParams, }; @@ -8605,9 +9843,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) previousSessionParams: Record | null, opts?: { useProjectWorkspace?: boolean | null }, ): Promise { - const issueId = readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); + const issueId = + readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); const contextProjectId = readNonEmptyString(context.projectId); - const contextProjectWorkspaceId = readNonEmptyString(context.projectWorkspaceId); + const contextProjectWorkspaceId = readNonEmptyString( + context.projectWorkspaceId, + ); const issueProjectRef = issueId ? await db .select({ @@ -8615,7 +9856,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) projectWorkspaceId: issues.projectWorkspaceId, }) .from(issues) - .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId))) + .where( + and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)), + ) .then((rows) => rows[0] ?? null) : null; const issueProjectId = issueProjectRef?.projectId ?? null; @@ -8651,24 +9894,28 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (projectWorkspaceRows.length > 0) { const preferredWorkspace = preferredProjectWorkspaceId - ? projectWorkspaceRows.find((workspace) => workspace.id === preferredProjectWorkspaceId) ?? null + ? (projectWorkspaceRows.find( + (workspace) => workspace.id === preferredProjectWorkspaceId, + ) ?? null) : null; const missingProjectCwds: string[] = []; const materializationFailures: WorkspaceMaterializationFailure[] = []; let hasConfiguredProjectCwd = false; let preferredWorkspaceWarning: string | null = null; if (preferredProjectWorkspaceId && !preferredWorkspace) { - preferredWorkspaceWarning = - `Selected project workspace "${preferredProjectWorkspaceId}" is not available on this project.`; + preferredWorkspaceWarning = `Selected project workspace "${preferredProjectWorkspaceId}" is not available on this project.`; } - const resolveGitAuth = createGitRemoteAuthProvider(db, agent.companyId, { issueId }); + const resolveGitAuth = createGitRemoteAuthProvider(db, agent.companyId, { + issueId, + }); for (const workspace of projectWorkspaceRows) { let projectCwd: string; let managedWorkspaceWarning: string | null = null; try { const resolvedCwd = await resolveConfiguredOrManagedProjectCwd({ companyId: agent.companyId, - projectId: workspaceProjectId ?? resolvedProjectId ?? workspace.projectId, + projectId: + workspaceProjectId ?? resolvedProjectId ?? workspace.projectId, cwd: workspace.cwd, repoUrl: workspace.repoUrl, resolveGitAuth, @@ -8682,7 +9929,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const workspaceRepoUrl = readNonEmptyString(workspace.repoUrl); materializationFailures.push({ projectWorkspaceId: workspace.id, - repoUrl: workspaceRepoUrl ? scrubGitCredentialText(workspaceRepoUrl) : null, + repoUrl: workspaceRepoUrl + ? scrubGitCredentialText(workspaceRepoUrl) + : null, error: scrubbedError, }); if (preferredWorkspace?.id === workspace.id) { @@ -8704,16 +9953,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) repoUrl: workspace.repoUrl, repoRef: workspace.repoRef, workspaceHints, - warnings: [preferredWorkspaceWarning, managedWorkspaceWarning].filter( - (value): value is string => Boolean(value), - ), + warnings: [ + preferredWorkspaceWarning, + managedWorkspaceWarning, + ].filter((value): value is string => Boolean(value)), baseCwdFallback: false, materializationFailures, }; } if (preferredWorkspace?.id === workspace.id) { - preferredWorkspaceWarning = - `Selected project workspace path "${projectCwd}" is not available yet.`; + preferredWorkspaceWarning = `Selected project workspace path "${projectCwd}" is not available yet.`; } missingProjectCwds.push(projectCwd); } @@ -8827,11 +10076,23 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) agent: typeof agents.$inferSelect, context: Record, previousSessionParams: Record | null, - opts?: { useProjectWorkspace?: boolean | null; executionEnvironmentDriver?: string | null }, + opts?: { + useProjectWorkspace?: boolean | null; + executionEnvironmentDriver?: string | null; + }, ): Promise { - const anchor = await resolveAnchorWorkspaceForRun(agent, context, previousSessionParams, opts); + const anchor = await resolveAnchorWorkspaceForRun( + agent, + context, + previousSessionParams, + opts, + ); if (!isMultiProjectWorkspaceSyncEnabled()) { - return { ...anchor, additionalWorkspaces: [], referencedProjectFailures: [] }; + return { + ...anchor, + additionalWorkspaces: [], + referencedProjectFailures: [], + }; } // Derive the remote-transport facts from the selected environment driver. `executionTargetIsRemote` @@ -8839,14 +10100,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // whether that remote target confines each staged tree (only the sandbox driver does). The remote // flag is the targeted kill switch; with it off, a remote run fails closed. const executionEnvironmentDriver = opts?.executionEnvironmentDriver ?? null; - const issueId = readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); - const { additionalWorkspaces, warnings, failures } = await resolveAdditionalRunWorkspaces( - issueId, - anchor.projectId, - { + const issueId = + readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); + const { additionalWorkspaces, warnings, failures } = + await resolveAdditionalRunWorkspaces(issueId, anchor.projectId, { enabled: true, - executionTargetIsRemote: isRemoteExecutionEnvironmentDriver(executionEnvironmentDriver), - targetStagesConfined: isConfinedRemoteStagingDriver(executionEnvironmentDriver), + executionTargetIsRemote: isRemoteExecutionEnvironmentDriver( + executionEnvironmentDriver, + ), + targetStagesConfined: isConfinedRemoteStagingDriver( + executionEnvironmentDriver, + ), remoteReferencedSyncEnabled: isMultiProjectWorkspaceSyncRemoteEnabled(), companyId: agent.companyId, actor: { @@ -8863,14 +10127,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) { companyId: agent.companyId, project }, defaultAdditionalProjectWorkspaceDeps(db), ), - }, - ); + }); return { ...anchor, additionalWorkspaces, referencedProjectFailures: failures, - warnings: warnings.length > 0 ? [...anchor.warnings, ...warnings] : anchor.warnings, + warnings: + warnings.length > 0 + ? [...anchor.warnings, ...warnings] + : anchor.warnings, }; } @@ -9019,7 +10285,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const updated = await db .update(heartbeatRuns) .set({ status, ...patch, updatedAt: new Date() }) - .where(and(eq(heartbeatRuns.id, runId), inArray(heartbeatRuns.status, fromStatuses))) + .where( + and( + eq(heartbeatRuns.id, runId), + inArray(heartbeatRuns.status, fromStatuses), + ), + ) .returning() .then((rows) => rows[0] ?? null); @@ -9064,8 +10335,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // already reached a terminal status, the run reached its goal, so use the // matching terminal run status. Otherwise the teardown cut the run short, // so use "interrupted". - const issueId = readNonEmptyString(parseObject(run.contextSnapshot).issueId); - let terminalStatus: "succeeded" | "cancelled" | "interrupted" = "interrupted"; + const issueId = readNonEmptyString( + parseObject(run.contextSnapshot).issueId, + ); + let terminalStatus: "succeeded" | "cancelled" | "interrupted" = + "interrupted"; if (issueId) { const issueStatus = await db .select({ status: issues.status }) @@ -9076,16 +10350,24 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) else if (issueStatus === "cancelled") terminalStatus = "cancelled"; } - const message = - `run terminalized on environment lease release: heartbeat_runs.status was still ${run.status} at teardown`; + const message = `run terminalized on environment lease release: heartbeat_runs.status was still ${run.status} at teardown`; // Match both "running" and "queued". A queued run has released its lease but // never reached "running", so a running-only update would miss it and leave // a phantom live run behind. - const write = await setRunStatusFromLive(run.id, terminalStatus, ["running", "queued"], { - finishedAt: run.finishedAt ?? new Date(), - error: run.error ?? (terminalStatus === "interrupted" ? message : null), - errorCode: run.errorCode ?? (terminalStatus === "interrupted" ? "lease_released_before_terminal" : null), - }); + const write = await setRunStatusFromLive( + run.id, + terminalStatus, + ["running", "queued"], + { + finishedAt: run.finishedAt ?? new Date(), + error: run.error ?? (terminalStatus === "interrupted" ? message : null), + errorCode: + run.errorCode ?? + (terminalStatus === "interrupted" + ? "lease_released_before_terminal" + : null), + }, + ); if (!write.updated) { // Another path already finalized the run. Keep that terminal outcome. return write.run ?? run; @@ -9114,7 +10396,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return terminalRun ?? run; } - function publishRunLifecyclePluginEvent(run: typeof heartbeatRuns.$inferSelect) { + function publishRunLifecyclePluginEvent( + run: typeof heartbeatRuns.$inferSelect, + ) { const eventType = run.status === "running" ? "agent.run.started" @@ -9143,11 +10427,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: run.triggerDetail, error: run.error ?? null, errorCode: run.errorCode ?? null, - issueId: typeof run.contextSnapshot === "object" && run.contextSnapshot !== null - ? (run.contextSnapshot as Record).issueId ?? null - : null, + issueId: + typeof run.contextSnapshot === "object" && + run.contextSnapshot !== null + ? ((run.contextSnapshot as Record).issueId ?? null) + : null, startedAt: run.startedAt ? new Date(run.startedAt).toISOString() : null, - finishedAt: run.finishedAt ? new Date(run.finishedAt).toISOString() : null, + finishedAt: run.finishedAt + ? new Date(run.finishedAt).toISOString() + : null, }, }); } @@ -9189,9 +10477,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); } - async function handleRunLivenessContinuation(run: typeof heartbeatRuns.$inferSelect) { + async function handleRunLivenessContinuation( + run: typeof heartbeatRuns.$inferSelect, + ) { const livenessState = run.livenessState as RunLivenessState | null; - if (livenessState !== "plan_only" && livenessState !== "empty_response") return; + if (livenessState !== "plan_only" && livenessState !== "empty_response") + return; const context = parseObject(run.contextSnapshot); const issueId = readNonEmptyString(context.issueId); @@ -9226,20 +10517,20 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const budgetBlock = issue && agent ? await budgets.getInvocationBlock(issue.companyId, agent.id, { - issueId: issue.id, - projectId: issue.projectId, - }) + issueId: issue.id, + projectId: issue.projectId, + }) : null; if (issue) { - const productivityHold = await productivityReviews.isProductivityReviewContinuationHoldActive({ - companyId: issue.companyId, - issueId: issue.id, - agentId: run.agentId, - }); + const productivityHold = + await productivityReviews.isProductivityReviewContinuationHoldActive({ + companyId: issue.companyId, + issueId: issue.id, + agentId: run.agentId, + }); if (productivityHold.held) { await setRunStatus(run.id, run.status, { - livenessReason: - `${run.livenessReason ?? "Run ended without concrete progress"}; continuation held by productivity review ${productivityHold.reviewIdentifier ?? productivityHold.reviewIssueId}`, + livenessReason: `${run.livenessReason ?? "Run ended without concrete progress"}; continuation held by productivity review ${productivityHold.reviewIdentifier ?? productivityHold.reviewIssueId}`, }); await productivityReviews.recordContinuationHold({ companyId: issue.companyId, @@ -9257,17 +10548,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const nextAttempt = readContinuationAttempt(run.continuationAttempt) + 1; const idempotencyKey = issue ? buildRunLivenessContinuationIdempotencyKey({ - issueId: issue.id, - sourceRunId: run.id, - livenessState, - nextAttempt, - }) + issueId: issue.id, + sourceRunId: run.id, + livenessState, + nextAttempt, + }) : null; const existingWake = idempotencyKey ? await findExistingRunLivenessContinuationWake(db, { - companyId: run.companyId, - idempotencyKey, - }) + companyId: run.companyId, + idempotencyKey, + }) : null; const decision = decideRunLivenessContinuation({ @@ -9317,20 +10608,28 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } - function issueUiLink(issue: Pick) { + function issueUiLink( + issue: Pick, + ) { const label = issue.identifier ?? issue.id; const prefix = issue.identifier?.split("-")[0] || "PAP"; return `[${label}](/${prefix}/issues/${label})`; } - function hasUnmanagedBackgroundTaskEvidence(resultJson: Record | null | undefined) { + function hasUnmanagedBackgroundTaskEvidence( + resultJson: Record | null | undefined, + ) { const evidence = parseObject(resultJson?.unmanagedBackgroundTask); - return evidence.stopped === true && + return ( + evidence.stopped === true && (evidence.stopReason === UNMANAGED_BACKGROUND_TASK_STOP_REASON || - evidence.reason === UNMANAGED_BACKGROUND_TASK_LIVENESS_REASON); + evidence.reason === UNMANAGED_BACKGROUND_TASK_LIVENESS_REASON) + ); } - function withUnmanagedBackgroundTaskStopReason(resultJson: Record | null | undefined) { + function withUnmanagedBackgroundTaskStopReason( + resultJson: Record | null | undefined, + ) { return { ...(resultJson ?? {}), stopReason: UNMANAGED_BACKGROUND_TASK_STOP_REASON, @@ -9343,8 +10642,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ) { const resultJson = parseObject(run.resultJson); const candidates = [ - hasUnmanagedBackgroundTaskEvidence(resultJson) ? UNMANAGED_BACKGROUND_TASK_LIVENESS_REASON : null, - readNonEmptyString(run.nextAction) ? `Next action noted: ${readNonEmptyString(run.nextAction)}` : null, + hasUnmanagedBackgroundTaskEvidence(resultJson) + ? UNMANAGED_BACKGROUND_TASK_LIVENESS_REASON + : null, + readNonEmptyString(run.nextAction) + ? `Next action noted: ${readNonEmptyString(run.nextAction)}` + : null, readNonEmptyString(run.livenessReason), readNonEmptyString(resultJson.summary), readNonEmptyString(resultJson.result), @@ -9359,7 +10662,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } async function addSuccessfulRunHandoffCommentOnce(input: { - issue: Pick; + issue: Pick< + typeof issues.$inferSelect, + "id" | "identifier" | "title" | "status" + >; run: typeof heartbeatRuns.$inferSelect; agent: Pick; detectedProgressSummary: string; @@ -9391,10 +10697,14 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ); } - async function handleSuccessfulRunHandoff(run: typeof heartbeatRuns.$inferSelect, agent: typeof agents.$inferSelect) { + async function handleSuccessfulRunHandoff( + run: typeof heartbeatRuns.$inferSelect, + agent: typeof agents.$inferSelect, + ) { if (run.status !== "succeeded") return; const context = parseObject(run.contextSnapshot); - const issueId = readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); + const issueId = + readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); if (!issueId) return; const issue = await db @@ -9417,9 +10727,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .then((rows) => rows[0] ?? null); const idempotencyKey = issue ? buildFinishSuccessfulRunHandoffIdempotencyKey({ - issueId: issue.id, - sourceRunId: run.id, - }) + issueId: issue.id, + sourceRunId: run.id, + }) : null; const taskKey = deriveTaskKeyWithHeartbeatFallback(context, null); const currentUserRedactionOptions = await getCurrentUserRedactionOptions(); @@ -9455,82 +10765,88 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ] = await Promise.all([ issue ? db - .select({ id: heartbeatRuns.id }) - .from(heartbeatRuns) - .where( - and( - eq(heartbeatRuns.companyId, issue.companyId), - eq(heartbeatRuns.agentId, run.agentId), - inArray(heartbeatRuns.status, [...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES]), - sql`( + .select({ id: heartbeatRuns.id }) + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.companyId, issue.companyId), + eq(heartbeatRuns.agentId, run.agentId), + inArray(heartbeatRuns.status, [ + ...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES, + ]), + sql`( ${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id} or ${heartbeatRuns.contextSnapshot} ->> 'taskId' = ${issue.id} )`, - sql`${heartbeatRuns.id} <> ${run.id}`, - ), - ) - .limit(1) - .then((rows) => rows[0] ?? null) + sql`${heartbeatRuns.id} <> ${run.id}`, + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) : Promise.resolve(null), issue ? db - .select({ id: agentWakeupRequests.id }) - .from(agentWakeupRequests) - .where( - and( - eq(agentWakeupRequests.companyId, issue.companyId), - eq(agentWakeupRequests.agentId, run.agentId), - inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution", "claimed"]), - sql`( + .select({ id: agentWakeupRequests.id }) + .from(agentWakeupRequests) + .where( + and( + eq(agentWakeupRequests.companyId, issue.companyId), + eq(agentWakeupRequests.agentId, run.agentId), + inArray(agentWakeupRequests.status, [ + "queued", + "deferred_issue_execution", + "claimed", + ]), + sql`( ${agentWakeupRequests.payload} ->> 'issueId' = ${issue.id} or ${agentWakeupRequests.payload} ->> 'taskId' = ${issue.id} or ${agentWakeupRequests.payload} -> '_paperclipWakeContext' ->> 'issueId' = ${issue.id} or ${agentWakeupRequests.payload} -> '_paperclipWakeContext' ->> 'taskId' = ${issue.id} )`, - ), - ) - .limit(1) - .then((rows) => rows[0] ?? null) + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) : Promise.resolve(null), issue ? db - .select({ id: issueThreadInteractions.id }) - .from(issueThreadInteractions) - .where( - and( - eq(issueThreadInteractions.companyId, issue.companyId), - eq(issueThreadInteractions.issueId, issue.id), - eq(issueThreadInteractions.status, "pending"), - ), - ) - .limit(1) - .then((rows) => rows[0] ?? null) + .select({ id: issueThreadInteractions.id }) + .from(issueThreadInteractions) + .where( + and( + eq(issueThreadInteractions.companyId, issue.companyId), + eq(issueThreadInteractions.issueId, issue.id), + eq(issueThreadInteractions.status, "pending"), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) : Promise.resolve(null), issue ? db - .select({ id: issueApprovals.approvalId }) - .from(issueApprovals) - .innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id)) - .where( - and( - eq(issueApprovals.companyId, issue.companyId), - eq(issueApprovals.issueId, issue.id), - inArray(approvals.status, ["pending", "revision_requested"]), - ), - ) - .limit(1) - .then((rows) => rows[0] ?? null) + .select({ id: issueApprovals.approvalId }) + .from(issueApprovals) + .innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id)) + .where( + and( + eq(issueApprovals.companyId, issue.companyId), + eq(issueApprovals.issueId, issue.id), + inArray(approvals.status, ["pending", "revision_requested"]), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) : Promise.resolve(null), issue ? db - .select({ id: issueRelations.issueId }) - .from(issueRelations) - .where( - and( - eq(issueRelations.companyId, issue.companyId), - eq(issueRelations.relatedIssueId, issue.id), - eq(issueRelations.type, "blocks"), - sql`exists ( + .select({ id: issueRelations.issueId }) + .from(issueRelations) + .where( + and( + eq(issueRelations.companyId, issue.companyId), + eq(issueRelations.relatedIssueId, issue.id), + eq(issueRelations.type, "blocks"), + sql`exists ( select 1 from issues blocker where blocker.id = ${issueRelations.issueId} @@ -9538,58 +10854,58 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) and blocker.status not in ('done', 'cancelled') and blocker.hidden_at is null )`, - ), - ) - .limit(1) - .then((rows) => rows[0] ?? null) + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) : Promise.resolve(null), issue ? db - .select({ id: issues.id }) - .from(issues) - .where( - and( - eq(issues.companyId, issue.companyId), - inArray(issues.originKind, [ - RECOVERY_ORIGIN_KINDS.strandedIssueRecovery, - RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation, - ]), - eq(issues.originId, issue.id), - visibleIssueCondition(), - notInArray(issues.status, ["done", "cancelled"]), - ), - ) - .limit(1) - .then((rows) => rows[0] ?? null) + .select({ id: issues.id }) + .from(issues) + .where( + and( + eq(issues.companyId, issue.companyId), + inArray(issues.originKind, [ + RECOVERY_ORIGIN_KINDS.strandedIssueRecovery, + RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation, + ]), + eq(issues.originId, issue.id), + visibleIssueCondition(), + notInArray(issues.status, ["done", "cancelled"]), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) : Promise.resolve(null), idempotencyKey ? findExistingFinishSuccessfulRunHandoffWake(db, { - companyId: run.companyId, - idempotencyKey, - }) + companyId: run.companyId, + idempotencyKey, + }) : Promise.resolve(null), issue ? budgets.getInvocationBlock(issue.companyId, run.agentId, { - issueId: issue.id, - projectId: issue.projectId, - }) + issueId: issue.id, + projectId: issue.projectId, + }) : Promise.resolve(null), issue ? treeControlSvc.getActivePauseHoldGate(issue.companyId, issue.id) : Promise.resolve(null), issue ? db - .select({ id: routines.id }) - .from(routines) - .where( - and( - eq(routines.companyId, issue.companyId), - eq(routines.parentIssueId, issue.id), - eq(routines.status, "active"), - ), - ) - .limit(1) - .then((rows) => rows[0] ?? null) + .select({ id: routines.id }) + .from(routines) + .where( + and( + eq(routines.companyId, issue.companyId), + eq(routines.parentIssueId, issue.id), + eq(routines.status, "active"), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) : Promise.resolve(null), ]); @@ -9604,7 +10920,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) taskKey, hasActiveExecutionPath: Boolean(activeExecutionPath), hasQueuedWake: Boolean(queuedWake), - hasPendingInteractionOrApproval: Boolean(pendingInteraction || pendingApproval), + hasPendingInteractionOrApproval: Boolean( + pendingInteraction || pendingApproval, + ), hasPersistedMonitor: Boolean(issue?.monitorNextCheckAt), hasExplicitBlockerPath: Boolean(explicitBlocker), hasOpenRecoveryIssue: Boolean(openRecoveryIssue), @@ -9632,7 +10950,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .update(heartbeatRuns) .set({ livenessReason: UNMANAGED_BACKGROUND_TASK_LIVENESS_REASON, - resultJson: withUnmanagedBackgroundTaskStopReason(parseObject(run.resultJson)), + resultJson: withUnmanagedBackgroundTaskStopReason( + parseObject(run.resultJson), + ), updatedAt: new Date(), }) .where(eq(heartbeatRuns.id, run.id)); @@ -9654,7 +10974,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) issue, run, agent, - detectedProgressSummary: detectedProgressSummary ?? "The run reported progress, but did not choose a next step.", + detectedProgressSummary: + detectedProgressSummary ?? + "The run reported progress, but did not choose a next step.", }); await logActivity(db, { companyId: issue.companyId, @@ -9677,9 +10999,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); } - async function handleIssueReviewPathDisposition(run: typeof heartbeatRuns.$inferSelect) { + async function handleIssueReviewPathDisposition( + run: typeof heartbeatRuns.$inferSelect, + ) { const contextSnapshot = parseObject(run.contextSnapshot); - const issueId = readNonEmptyString(contextSnapshot.issueId) ?? readNonEmptyString(contextSnapshot.taskId); + const issueId = + readNonEmptyString(contextSnapshot.issueId) ?? + readNonEmptyString(contextSnapshot.taskId); if (!issueId) return; const issue = await db @@ -9693,11 +11019,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .from(issues) .where(and(eq(issues.id, issueId), eq(issues.companyId, run.companyId))) .then((rows) => rows[0] ?? null); - if (!issue || issue.status !== "in_review" || !issue.assigneeAgentId) return; + if (!issue || issue.status !== "in_review" || !issue.assigneeAgentId) + return; const reviewAttention = await issuesSvc .listReviewAttention(issue.companyId, [issue]) - .then((map) => map.get(issue.id) ?? { state: "none" as const, paths: [], reason: null }); + .then( + (map) => + map.get(issue.id) ?? { + state: "none" as const, + paths: [], + reason: null, + }, + ); if (reviewAttention.state !== "stalled") return; const consumedPathRef = reviewPathConsumedRefFromRun({ @@ -9712,11 +11046,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const existingWake = await db .select({ id: agentWakeupRequests.id }) .from(agentWakeupRequests) - .where(and( - eq(agentWakeupRequests.companyId, issue.companyId), - eq(agentWakeupRequests.idempotencyKey, idempotencyKey), - notInArray(agentWakeupRequests.status, ["skipped"]), - )) + .where( + and( + eq(agentWakeupRequests.companyId, issue.companyId), + eq(agentWakeupRequests.idempotencyKey, idempotencyKey), + notInArray(agentWakeupRequests.status, ["skipped"]), + ), + ) .limit(1) .then((rows) => rows[0] ?? null); @@ -9784,9 +11120,14 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const boundedPayload = event.payload ? boundHeartbeatRunEventPayloadForStorage(event.payload) : event.payload; - const secretSanitizedPayload = boundedPayload ? redactEventPayload(boundedPayload) : boundedPayload; + const secretSanitizedPayload = boundedPayload + ? redactEventPayload(boundedPayload) + : boundedPayload; const sanitizedPayload = secretSanitizedPayload - ? redactCurrentUserValue(secretSanitizedPayload, currentUserRedactionOptions) + ? redactCurrentUserValue( + secretSanitizedPayload, + currentUserRedactionOptions, + ) : secretSanitizedPayload; const issueId = readRuntimeStatusIssueIdCandidate(run) ?? null; const progress = buildRunEventRuntimeProgress({ @@ -9868,7 +11209,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) errorCode: null, updatedAt: new Date(), }) - .where(and(eq(heartbeatRuns.id, runId), eq(heartbeatRuns.status, "running"), eq(heartbeatRuns.errorCode, DETACHED_PROCESS_ERROR_CODE))) + .where( + and( + eq(heartbeatRuns.id, runId), + eq(heartbeatRuns.status, "running"), + eq(heartbeatRuns.errorCode, DETACHED_PROCESS_ERROR_CODE), + ), + ) .returning() .then((rows) => rows[0] ?? null); if (!updated) return null; @@ -9877,14 +11224,22 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "info", - message: "Detached child process reported activity; cleared detached warning", + message: + "Detached child process reported activity; cleared detached warning", }); return updated; } async function patchRunIssueCommentStatus( runId: string, - patch: Partial>, + patch: Partial< + Pick< + typeof heartbeatRuns.$inferInsert, + | "issueCommentStatus" + | "issueCommentSatisfiedByCommentId" + | "issueCommentRetryQueuedAt" + > + >, ) { return db .update(heartbeatRuns) @@ -9903,6 +11258,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const comments = await db .select({ id: issueComments.id, + body: issueComments.body, }) .from(issueComments) .where( @@ -9916,6 +11272,37 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return findHeartbeatRunCompletionComment(comments, resultJson); } + async function findLatestCompletedFinalAgentMessage( + runId: string, + companyId: string, + ) { + const rows = await db + .select({ payload: heartbeatRunEvents.payload }) + .from(heartbeatRunEvents) + .where( + and( + eq(heartbeatRunEvents.companyId, companyId), + eq(heartbeatRunEvents.runId, runId), + eq(heartbeatRunEvents.eventType, "item.completed"), + ), + ) + .orderBy(desc(heartbeatRunEvents.seq)) + .limit(200); + for (const row of rows) { + const prpEvent = parseObject(parseObject(row.payload).prpEvent); + const payload = parseObject(prpEvent.payload); + if (payload.kind !== "agentMessage" || payload.channel !== "final") + continue; + const text = readNonEmptyString(payload.text); + if (!text) continue; + return { + text, + sourceEventId: readNonEmptyString(prpEvent.sourceEventId), + }; + } + return null; + } + async function refreshContinuationSummaryForRun( run: typeof heartbeatRuns.$inferSelect, agent: typeof agents.$inferSelect, @@ -9968,7 +11355,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "warn", - message: "Missing-comment retry suppressed because the agent is not invokable", + message: + "Missing-comment retry suppressed because the agent is not invokable", payload: { reason: invokability.reason, invalidOrgChain: invokability.invalidOrgChain, @@ -9981,14 +11369,20 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const contextSnapshot = parseObject(run.contextSnapshot); const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null); const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey); - const retryContextSnapshot = withRecoveryModelProfileHint({ - ...contextSnapshot, - retryOfRunId: run.id, - wakeReason: "missing_issue_comment", - retryReason: "missing_issue_comment", - missingIssueCommentForRunId: run.id, - }, "status_only"); - const responsibleUserId = await resolveResponsibleUserIdForRunContext(run, retryContextSnapshot); + const retryContextSnapshot = withRecoveryModelProfileHint( + { + ...contextSnapshot, + retryOfRunId: run.id, + wakeReason: "missing_issue_comment", + retryReason: "missing_issue_comment", + missingIssueCommentForRunId: run.id, + }, + "status_only", + ); + const responsibleUserId = await resolveResponsibleUserIdForRunContext( + run, + retryContextSnapshot, + ); const now = new Date(); const retryRun = await db.transaction(async (tx) => { @@ -9999,7 +11393,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const issue = await tx .select({ id: issues.id }) .from(issues) - .where(and(eq(issues.companyId, run.companyId), eq(issues.executionRunId, run.id))) + .where( + and( + eq(issues.companyId, run.companyId), + eq(issues.executionRunId, run.id), + ), + ) .then((rows) => rows[0] ?? null); if (!issue) return null; @@ -10011,11 +11410,14 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "automation", triggerDetail: "system", reason: "missing_issue_comment", - payload: withRecoveryModelProfileHint({ - issueId, - retryOfRunId: run.id, - retryReason: "missing_issue_comment", - }, "status_only"), + payload: withRecoveryModelProfileHint( + { + issueId, + retryOfRunId: run.id, + retryReason: "missing_issue_comment", + }, + "status_only", + ), status: "queued", requestedByActorType: "system", requestedByActorId: null, @@ -10090,7 +11492,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return retryRun; } - async function hasDeferredIssueCommentWake(companyId: string, issueId: string, agentId: string) { + async function hasDeferredIssueCommentWake( + companyId: string, + issueId: string, + agentId: string, + ) { const deferredPayloads = await db .select({ payload: agentWakeupRequests.payload }) .from(agentWakeupRequests) @@ -10105,7 +11511,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return deferredPayloads.some(({ payload }) => { const parsedPayload = parseObject(payload); - const deferredContext = parseObject(parsedPayload[DEFERRED_WAKE_CONTEXT_KEY]); + const deferredContext = parseObject( + parsedPayload[DEFERRED_WAKE_CONTEXT_KEY], + ); return Boolean(deriveCommentId(deferredContext, parsedPayload)); }); } @@ -10113,6 +11521,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) async function finalizeIssueCommentPolicy( run: typeof heartbeatRuns.$inferSelect, agent: typeof agents.$inferSelect, + presentationDecision?: RunPresentationDecision | null, ) { const contextSnapshot = parseObject(run.contextSnapshot); const issueId = readNonEmptyString(contextSnapshot.issueId); @@ -10127,6 +11536,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { outcome: "not_applicable" as const, queuedRun: null }; } + // A settled run may legitimately have no user-facing prose. The response + // resolver owns that decision; do not wake the agent again merely to force + // an artificial comment into the issue thread. + if (presentationDecision?.chosenSource === "none") { + await patchRunIssueCommentStatus(run.id, { + issueCommentStatus: "not_applicable", + issueCommentSatisfiedByCommentId: null, + issueCommentRetryQueuedAt: null, + }); + return { outcome: "not_applicable" as const, queuedRun: null }; + } + const postedComment = await findRunIssueComment( run.id, run.companyId, @@ -10142,7 +11563,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { outcome: "satisfied" as const, queuedRun: null }; } - if (readNonEmptyString(contextSnapshot.retryReason) === "missing_issue_comment") { + if ( + readNonEmptyString(contextSnapshot.retryReason) === + "missing_issue_comment" + ) { await patchRunIssueCommentStatus(run.id, { issueCommentStatus: "retry_exhausted", issueCommentSatisfiedByCommentId: null, @@ -10151,7 +11575,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "warn", - message: "Run ended without an issue comment after one retry; no further comment wake will be queued", + message: + "Run ended without an issue comment after one retry; no further comment wake will be queued", }); return { outcome: "retry_exhausted" as const, queuedRun: null }; } @@ -10167,7 +11592,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { outcome: "not_applicable" as const, queuedRun: null }; } - if (await hasDeferredIssueCommentWake(run.companyId, issueId, run.agentId)) { + if ( + await hasDeferredIssueCommentWake(run.companyId, issueId, run.agentId) + ) { await patchRunIssueCommentStatus(run.id, { issueCommentStatus: "not_applicable", issueCommentSatisfiedByCommentId: null, @@ -10177,18 +11604,24 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "info", - message: "Run ended without an issue comment; a deferred comment wake already exists for this issue", + message: + "Run ended without an issue comment; a deferred comment wake already exists for this issue", }); return { outcome: "not_applicable" as const, queuedRun: null }; } - const queuedRun = await enqueueMissingIssueCommentRetry(run, agent, issueId); + const queuedRun = await enqueueMissingIssueCommentRetry( + run, + agent, + issueId, + ); if (queuedRun) { await appendRunEvent(run, await nextRunEventSeq(run.id), { eventType: "lifecycle", stream: "system", level: "warn", - message: "Run ended without an issue comment; queued one follow-up wake to require a comment", + message: + "Run ended without an issue comment; queued one follow-up wake to require a comment", }); return { outcome: "retry_queued" as const, queuedRun }; } @@ -10208,7 +11641,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const existingRetry = await db .select() .from(heartbeatRuns) - .where(and(eq(heartbeatRuns.companyId, run.companyId), eq(heartbeatRuns.retryOfRunId, run.id))) + .where( + and( + eq(heartbeatRuns.companyId, run.companyId), + eq(heartbeatRuns.retryOfRunId, run.id), + ), + ) .orderBy(asc(heartbeatRuns.createdAt)) .limit(1) .then((rows) => rows[0] ?? null); @@ -10217,7 +11655,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "warn", - message: "Process-loss retry already exists; skipping duplicate retry enqueue", + message: + "Process-loss retry already exists; skipping duplicate retry enqueue", payload: { retryRunId: existingRetry.id, retryRunStatus: existingRetry.status, @@ -10232,7 +11671,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "warn", - message: "Process-loss retry suppressed because the agent is not invokable", + message: + "Process-loss retry suppressed because the agent is not invokable", payload: { reason: invokability.reason, invalidOrgChain: invokability.invalidOrgChain, @@ -10245,18 +11685,25 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const contextSnapshot = parseObject(run.contextSnapshot); const issueId = readNonEmptyString(contextSnapshot.issueId); - const retryReason = readNonEmptyString(contextSnapshot.wakeReason) === "issue_monitor_due" - ? "issue_continuation_needed" - : "process_lost"; + const retryReason = + readNonEmptyString(contextSnapshot.wakeReason) === "issue_monitor_due" + ? "issue_continuation_needed" + : "process_lost"; const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null); const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey); - const retryContextSnapshot = withRecoveryModelProfileHint({ - ...contextSnapshot, - retryOfRunId: run.id, - wakeReason: "process_lost_retry", - retryReason, - }, "normal_model"); - const responsibleUserId = await resolveResponsibleUserIdForRunContext(run, retryContextSnapshot); + const retryContextSnapshot = withRecoveryModelProfileHint( + { + ...contextSnapshot, + retryOfRunId: run.id, + wakeReason: "process_lost_retry", + retryReason, + }, + "normal_model", + ); + const responsibleUserId = await resolveResponsibleUserIdForRunContext( + run, + retryContextSnapshot, + ); const queued = await db.transaction(async (tx) => { const wakeupRequest = await tx @@ -10267,10 +11714,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "automation", triggerDetail: "system", reason: "process_lost_retry", - payload: withRecoveryModelProfileHint({ - ...(issueId ? { issueId } : {}), - retryOfRunId: run.id, - }, "normal_model"), + payload: withRecoveryModelProfileHint( + { + ...(issueId ? { issueId } : {}), + retryOfRunId: run.id, + }, + "normal_model", + ), status: "queued", requestedByActorType: "system", requestedByActorId: null, @@ -10316,7 +11766,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) executionLockedAt: now, updatedAt: now, }) - .where(and(eq(issues.id, issueId), eq(issues.companyId, run.companyId), eq(issues.executionRunId, run.id))); + .where( + and( + eq(issues.id, issueId), + eq(issues.companyId, run.companyId), + eq(issues.executionRunId, run.id), + ), + ); } return retryRun; @@ -10338,7 +11794,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "warn", - message: "Queued automatic retry after orphaned child process was confirmed dead", + message: + "Queued automatic retry after orphaned child process was confirmed dead", payload: { retryOfRunId: run.id, }, @@ -10370,35 +11827,71 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) adapterConfig: unknown; }) { const context = parseObject(input.run.contextSnapshot); - if (context.processTopology === "server_stdio" || context.executionEngine === "acp") { + if ( + context.processTopology === "server_stdio" || + context.executionEngine === "acp" + ) { return true; } - if (context.processTopology === "detached" || context.executionEngine === "cli") { + if ( + context.processTopology === "detached" || + context.executionEngine === "cli" + ) { return false; } - if (!["claude_local", "codex_local", "gemini_local"].includes(input.adapterType)) { + if ( + !["claude_local", "codex_local", "gemini_local"].includes( + input.adapterType, + ) + ) { return false; } - return readNonEmptyString(parseObject(input.adapterConfig).engine) !== "cli"; + return ( + readNonEmptyString(parseObject(input.adapterConfig).engine) !== "cli" + ); } - async function prepareHotRestartShutdown(signal: "SIGINT" | "SIGTERM", now = new Date()) { + async function prepareHotRestartShutdown( + signal: "SIGINT" | "SIGTERM", + now = new Date(), + ) { let intent: Awaited>; try { intent = await readHotRestartIntent(); } catch (err) { - logger.warn({ err }, "failed to read hot-restart intent; falling back to normal shutdown drain"); - return { mode: "read_error" as const, skipDrain: false as const, activeRunIds: [] as string[] }; + logger.warn( + { err }, + "failed to read hot-restart intent; falling back to normal shutdown drain", + ); + return { + mode: "read_error" as const, + skipDrain: false as const, + activeRunIds: [] as string[], + }; } - if (!intent) return { mode: "not_requested" as const, skipDrain: false as const, activeRunIds: [] as string[] }; - if (intent.drainRequired) return { mode: "drain_required" as const, skipDrain: false as const, activeRunIds: [] as string[] }; + if (!intent) + return { + mode: "not_requested" as const, + skipDrain: false as const, + activeRunIds: [] as string[], + }; + if (intent.drainRequired) + return { + mode: "drain_required" as const, + skipDrain: false as const, + activeRunIds: [] as string[], + }; if (!shouldHonorHotRestartIntentForProcess(intent)) { logger.warn( { expectedPid: intent.previousServerPid, currentPid: process.pid }, "hot-restart intent targets a different server pid; falling back to normal shutdown drain", ); - return { mode: "pid_mismatch" as const, skipDrain: false as const, activeRunIds: [] as string[] }; + return { + mode: "pid_mismatch" as const, + skipDrain: false as const, + activeRunIds: [] as string[], + }; } const activeRuns = await db @@ -10461,7 +11954,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "info", - message: "Hot restart requested; leaving child process alive for startup adoption", + message: + "Hot restart requested; leaving child process alive for startup adoption", payload: { signal, previousServerPid: intent.previousServerPid, @@ -10473,7 +11967,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } logger.info( - { signal, previousServerPid: intent.previousServerPid, activeRunIds: snapshotRuns.map((run) => run.runId) }, + { + signal, + previousServerPid: intent.previousServerPid, + activeRunIds: snapshotRuns.map((run) => run.runId), + }, "hot-restart shutdown snapshot captured; skipping graceful run drain", ); @@ -10489,7 +11987,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) try { intent = await readHotRestartIntent(); } catch (err) { - logger.warn({ err }, "failed to read hot-restart intent on startup; skipping adoption"); + logger.warn( + { err }, + "failed to read hot-restart intent on startup; skipping adoption", + ); return { mode: "read_error" as const, adoptedRunIds: [] as string[], @@ -10509,7 +12010,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } if (!intent.shutdownSnapshot) { - const log = intent.drainRequired ? logger.info.bind(logger) : logger.warn.bind(logger); + const log = intent.drainRequired + ? logger.info.bind(logger) + : logger.warn.bind(logger); log( { previousServerPid: intent.previousServerPid, @@ -10524,18 +12027,22 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const candidates = intent.shutdownSnapshot?.activeRuns ?? []; const missingSnapshotRunIds = findMissingHotRestartSnapshotRunIds(intent); const reconciliationRunIds = [ - ...new Set([...candidates.map((run) => run.runId), ...missingSnapshotRunIds]), + ...new Set([ + ...candidates.map((run) => run.runId), + ...missingSnapshotRunIds, + ]), ]; - const currentRows = reconciliationRunIds.length > 0 - ? await db - .select({ - run: heartbeatRuns, - adapterType: agents.adapterType, - }) - .from(heartbeatRuns) - .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id)) - .where(inArray(heartbeatRuns.id, reconciliationRunIds)) - : []; + const currentRows = + reconciliationRunIds.length > 0 + ? await db + .select({ + run: heartbeatRuns, + adapterType: agents.adapterType, + }) + .from(heartbeatRuns) + .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id)) + .where(inArray(heartbeatRuns.id, reconciliationRunIds)) + : []; const currentByRunId = new Map(currentRows.map((row) => [row.run.id, row])); const reportRuns: HotRestartReportRun[] = []; @@ -10550,10 +12057,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) reason: string, patch?: Partial, ) => { - const run = { ...candidate, ...patch, classification, reason } satisfies HotRestartReportRun; + const run = { + ...candidate, + ...patch, + classification, + reason, + } satisfies HotRestartReportRun; reportRuns.push(run); if (classification === "adopted") adoptedRunIds.push(candidate.runId); - else if (classification === "finalized_while_down") finalizedWhileDownRunIds.push(candidate.runId); + else if (classification === "finalized_while_down") + finalizedWhileDownRunIds.push(candidate.runId); else if (classification === "lost") lostRunIds.push(candidate.runId); else skippedRunIds.push(candidate.runId); }; @@ -10567,7 +12080,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const candidate = toHotRestartIntentRun(current); if (current.run.status !== "running") { - classify(candidate, "finalized_while_down", `run_status_${current.run.status}`); + classify( + candidate, + "finalized_while_down", + `run_status_${current.run.status}`, + ); } else { classify(candidate, "lost", "missing_shutdown_snapshot"); } @@ -10596,13 +12113,22 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; if (run.status !== "running") { - classify(candidate, "finalized_while_down", `run_status_${run.status}`, patch); + classify( + candidate, + "finalized_while_down", + `run_status_${run.status}`, + patch, + ); continue; } - const hasSelectiveAcpDrain = intent.drainReason === "active_acp_run" - && (intent.drainRunIds?.length ?? 0) > 0; - if (hasSelectiveAcpDrain && intent.drainRunIds?.includes(candidate.runId)) { + const hasSelectiveAcpDrain = + intent.drainReason === "active_acp_run" && + (intent.drainRunIds?.length ?? 0) > 0; + if ( + hasSelectiveAcpDrain && + intent.drainRunIds?.includes(candidate.runId) + ) { // A selective ACP drain is expected to persist a terminal row before // the new server starts. If the process was terminated but that write // failed, surface the run as lost instead of hiding it as an expected @@ -10610,16 +12136,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) classify(candidate, "lost", "selective_drain_not_finalized", patch); continue; } - if ( - intent.drainRequired - && !hasSelectiveAcpDrain - ) { + if (intent.drainRequired && !hasSelectiveAcpDrain) { classify(candidate, "skipped", "drain_required", patch); continue; } if (!isTrackedLocalChildProcessAdapter(adapterType)) { - classify(candidate, "skipped", "adapter_not_local_child_process", patch); + classify( + candidate, + "skipped", + "adapter_not_local_child_process", + patch, + ); continue; } @@ -10636,24 +12164,36 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) continue; } - const resultJson = mergeHotRestartAdoptionResultJson(parseObject(run.resultJson), { - adoptedAt: now, - previousServerPid: intent.previousServerPid, - newServerPid: process.pid, - previousServerVersion: intent.previousServerVersion, - newServerVersion: serverVersion, - processPid, - processGroupId, - }); + const resultJson = mergeHotRestartAdoptionResultJson( + parseObject(run.resultJson), + { + adoptedAt: now, + previousServerPid: intent.previousServerPid, + newServerPid: process.pid, + previousServerVersion: intent.previousServerVersion, + newServerVersion: serverVersion, + processPid, + processGroupId, + }, + ); const updated = await db .update(heartbeatRuns) .set({ resultJson, - error: run.errorCode === DETACHED_PROCESS_ERROR_CODE ? null : run.error, - errorCode: run.errorCode === DETACHED_PROCESS_ERROR_CODE ? null : run.errorCode, + error: + run.errorCode === DETACHED_PROCESS_ERROR_CODE ? null : run.error, + errorCode: + run.errorCode === DETACHED_PROCESS_ERROR_CODE + ? null + : run.errorCode, updatedAt: now, }) - .where(and(eq(heartbeatRuns.id, run.id), eq(heartbeatRuns.status, "running"))) + .where( + and( + eq(heartbeatRuns.id, run.id), + eq(heartbeatRuns.status, "running"), + ), + ) .returning() .then((rows) => rows[0] ?? null); @@ -10664,7 +12204,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .where(eq(heartbeatRuns.id, run.id)) .then((rows) => rows[0] ?? null); if (latest && latest.status !== "running") { - classify(candidate, "finalized_while_down", `run_status_${latest.status}`, patch); + classify( + candidate, + "finalized_while_down", + `run_status_${latest.status}`, + patch, + ); } else { classify(candidate, "lost", "adoption_update_not_applied", patch); } @@ -10685,7 +12230,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) processGroupId, }, }); - classify(candidate, "adopted", processPidAlive ? "process_pid_alive" : "process_group_alive", patch); + classify( + candidate, + "adopted", + processPidAlive ? "process_pid_alive" : "process_group_alive", + patch, + ); } const report = await writeHotRestartReport({ @@ -10693,7 +12243,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) requestedAt: intent.requestedAt, completedAt: now.toISOString(), drainRequired: intent.drainRequired, - drainReason: intent.drainReason ?? (intent.drainRequired ? "requested" : null), + drainReason: + intent.drainReason ?? (intent.drainRequired ? "requested" : null), previousServerPid: intent.previousServerPid, newServerPid: process.pid, previousServerVersion: intent.previousServerVersion, @@ -10747,9 +12298,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .where( selectedRunIds ? and( - eq(heartbeatRuns.status, "running"), - inArray(heartbeatRuns.id, selectedRunIds), - ) + eq(heartbeatRuns.status, "running"), + inArray(heartbeatRuns.id, selectedRunIds), + ) : eq(heartbeatRuns.status, "running"), ); @@ -10776,24 +12327,32 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const message = `Interrupted by graceful server shutdown (${signal}); retry queued for restart recovery`; - const interruptedStatus = await setRunStatusIfRunning(run.id, "interrupted", { - finishedAt: now, - error: message, - errorCode: "server_shutdown_interrupted", - signal, - resultJson: mergeRunStopMetadataForAgent(agent, "interrupted", { - resultJson: parseObject(run.resultJson), + const interruptedStatus = await setRunStatusIfRunning( + run.id, + "interrupted", + { + finishedAt: now, + error: message, errorCode: "server_shutdown_interrupted", - errorMessage: message, - }), - }); + signal, + resultJson: mergeRunStopMetadataForAgent(agent, "interrupted", { + resultJson: parseObject(run.resultJson), + errorCode: "server_shutdown_interrupted", + errorMessage: message, + }), + }, + ); if (!interruptedStatus.updated || !interruptedStatus.run) continue; let interrupted = interruptedStatus.run; await setWakeupStatus(run.wakeupRequestId, "cancelled", { finishedAt: now, error: null, }); - interrupted = await classifyAndPersistRunLiveness(interrupted, parseObject(interrupted.resultJson)) ?? interrupted; + interrupted = + (await classifyAndPersistRunLiveness( + interrupted, + parseObject(interrupted.resultJson), + )) ?? interrupted; await releaseEnvironmentLeasesForRun({ runId: interrupted.id, @@ -10831,7 +12390,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (interruptedRunIds.length > 0) { logger.warn( - { signal, interrupted: interruptedRunIds.length, interruptedRunIds, retryRunIds }, + { + signal, + interrupted: interruptedRunIds.length, + interruptedRunIds, + retryRunIds, + }, "interrupted running heartbeat runs for graceful shutdown", ); } @@ -10863,7 +12427,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) issueId: string | null; details: Record; }; - type BlockedScheduledRetryGate = Extract; + type BlockedScheduledRetryGate = Extract< + ScheduledRetryGate, + { allowed: false } + >; async function evaluateScheduledRetryGate(input: { run: typeof heartbeatRuns.$inferSelect; @@ -10874,14 +12441,21 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }): Promise { const { run, agent, contextSnapshot } = input; const retryReason = - input.retryReason ?? readNonEmptyString(contextSnapshot.retryReason) ?? run.scheduledRetryReason ?? null; + input.retryReason ?? + readNonEmptyString(contextSnapshot.retryReason) ?? + run.scheduledRetryReason ?? + null; const issueId = readNonEmptyString(contextSnapshot.issueId); const projectId = readNonEmptyString(contextSnapshot.projectId); - const budgetBlock = await budgets.getInvocationBlock(run.companyId, run.agentId, { - issueId, - projectId, - }); + const budgetBlock = await budgets.getInvocationBlock( + run.companyId, + run.agentId, + { + issueId, + projectId, + }, + ); if (budgetBlock) { return { allowed: false, @@ -10926,7 +12500,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (!issue) { return { allowed: false, - reason: "Scheduled retry suppressed because the target issue no longer exists", + reason: + "Scheduled retry suppressed because the target issue no longer exists", errorCode: "issue_not_found", issueId, details: { issueId }, @@ -10953,19 +12528,29 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { allowed: false, reason: `Scheduled retry suppressed because issue reached terminal status (${issue.status})`, - errorCode: issue.status === "cancelled" ? "issue_cancelled" : "issue_terminal_status", + errorCode: + issue.status === "cancelled" + ? "issue_cancelled" + : "issue_terminal_status", issueId, details: { issueId, currentStatus: issue.status }, }; } - if (retryReason === MAX_TURN_CONTINUATION_RETRY_REASON && issue.status !== "in_progress") { + if ( + retryReason === MAX_TURN_CONTINUATION_RETRY_REASON && + issue.status !== "in_progress" + ) { return { allowed: false, reason: `Scheduled max-turn continuation suppressed because issue is no longer in_progress (current status: ${issue.status})`, errorCode: "issue_not_in_progress", issueId, - details: { issueId, currentStatus: issue.status, requiredStatus: "in_progress" }, + details: { + issueId, + currentStatus: issue.status, + requiredStatus: "in_progress", + }, }; } @@ -10976,7 +12561,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ) { return { allowed: false, - reason: "Scheduled max-turn continuation suppressed because the issue execution lock belongs to a different run", + reason: + "Scheduled max-turn continuation suppressed because the issue execution lock belongs to a different run", errorCode: "issue_execution_lock_changed", issueId, details: { @@ -10992,11 +12578,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const currentParticipant = executionState?.currentParticipant ?? null; if (currentParticipant) { const participantMatches = - currentParticipant.type === "agent" && currentParticipant.agentId === run.agentId; + currentParticipant.type === "agent" && + currentParticipant.agentId === run.agentId; if (!participantMatches) { return { allowed: false, - reason: "Scheduled retry suppressed because the issue is waiting on another review participant", + reason: + "Scheduled retry suppressed because the issue is waiting on another review participant", errorCode: "issue_review_participant_changed", issueId, details: { @@ -11009,11 +12597,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } - const activePauseHold = await treeControlSvc.getActivePauseHoldGate(run.companyId, issueId); + const activePauseHold = await treeControlSvc.getActivePauseHoldGate( + run.companyId, + issueId, + ); if (activePauseHold) { return { allowed: false, - reason: "Scheduled retry suppressed because the issue is held by an active subtree pause hold", + reason: + "Scheduled retry suppressed because the issue is held by an active subtree pause hold", errorCode: "issue_paused", issueId, details: { @@ -11024,12 +12616,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; } - const dependencyReadiness = await issuesSvc.listDependencyReadiness(run.companyId, [issueId]); + const dependencyReadiness = await issuesSvc.listDependencyReadiness( + run.companyId, + [issueId], + ); const readiness = dependencyReadiness.get(issueId); if (readiness && !readiness.isDependencyReady) { return { allowed: false, - reason: "Scheduled retry suppressed because issue dependencies are still blocked", + reason: + "Scheduled retry suppressed because issue dependencies are still blocked", errorCode: "issue_dependencies_blocked", issueId, details: { @@ -11107,7 +12703,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) payload: { ...gate.details, scheduledRetryAttempt: cancelled.scheduledRetryAttempt, - scheduledRetryAt: cancelled.scheduledRetryAt ? new Date(cancelled.scheduledRetryAt).toISOString() : null, + scheduledRetryAt: cancelled.scheduledRetryAt + ? new Date(cancelled.scheduledRetryAt).toISOString() + : null, scheduledRetryReason: cancelled.scheduledRetryReason, }, }); @@ -11134,7 +12732,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) allowed: false as const, reason: "Scheduled retry suppressed because the agent no longer exists", errorCode: "agent_not_invokable" as const, - issueId: readNonEmptyString(parseObject(dueRun.contextSnapshot).issueId), + issueId: readNonEmptyString( + parseObject(dueRun.contextSnapshot).issueId, + ), details: { agentId: dueRun.agentId }, }; const cancelled = await cancelScheduledRetryForGate(dueRun, gate, now); @@ -11154,7 +12754,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) agent, contextSnapshot, retryReason: dueRun.scheduledRetryReason, - enforceIssueExecutionLock: dueRun.scheduledRetryReason === MAX_TURN_CONTINUATION_RETRY_REASON, + enforceIssueExecutionLock: + dueRun.scheduledRetryReason === MAX_TURN_CONTINUATION_RETRY_REASON, }); if (!gate.allowed) { if ( @@ -11197,10 +12798,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "info", - message: "Scheduled retry became due and was promoted to the queued run pool", + message: + "Scheduled retry became due and was promoted to the queued run pool", payload: { scheduledRetryAttempt: promoted.scheduledRetryAttempt, - scheduledRetryAt: promoted.scheduledRetryAt ? new Date(promoted.scheduledRetryAt).toISOString() : null, + scheduledRetryAt: promoted.scheduledRetryAt + ? new Date(promoted.scheduledRetryAt).toISOString() + : null, scheduledRetryReason: promoted.scheduledRetryReason, }, }); @@ -11233,30 +12837,47 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }, ) { const now = opts?.now ?? new Date(); - const retryReason = opts?.retryReason ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_REASON; - const wakeReason = opts?.wakeReason ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_WAKE_REASON; - const maxAttempts = Math.max(0, Math.floor(opts?.maxAttempts ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS)); + const retryReason = + opts?.retryReason ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_REASON; + const wakeReason = + opts?.wakeReason ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_WAKE_REASON; + const maxAttempts = Math.max( + 0, + Math.floor( + opts?.maxAttempts ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS, + ), + ); const nextAttempt = (run.scheduledRetryAttempt ?? 0) + 1; - const computedBaseSchedule = opts?.delayMs != null - ? nextAttempt <= maxAttempts - ? { - attempt: nextAttempt, - baseDelayMs: Math.max(0, Math.floor(opts.delayMs)), - delayMs: Math.max(0, Math.floor(opts.delayMs)), - dueAt: new Date(now.getTime() + Math.max(0, Math.floor(opts.delayMs))), - maxAttempts, - } - : null - : nextAttempt <= maxAttempts - ? computeBoundedTransientHeartbeatRetrySchedule(nextAttempt, now, opts?.random) - : null; - const baseSchedule = computedBaseSchedule ? { ...computedBaseSchedule, maxAttempts } : null; + const computedBaseSchedule = + opts?.delayMs != null + ? nextAttempt <= maxAttempts + ? { + attempt: nextAttempt, + baseDelayMs: Math.max(0, Math.floor(opts.delayMs)), + delayMs: Math.max(0, Math.floor(opts.delayMs)), + dueAt: new Date( + now.getTime() + Math.max(0, Math.floor(opts.delayMs)), + ), + maxAttempts, + } + : null + : nextAttempt <= maxAttempts + ? computeBoundedTransientHeartbeatRetrySchedule( + nextAttempt, + now, + opts?.random, + ) + : null; + const baseSchedule = computedBaseSchedule + ? { ...computedBaseSchedule, maxAttempts } + : null; const transientRecovery = retryReason === BOUNDED_TRANSIENT_HEARTBEAT_RETRY_REASON ? readTransientRecoveryContractFromRun(run) : null; const codexTransientFallbackMode = - agent.adapterType === "codex_local" && transientRecovery?.errorFamily === "transient_upstream" + agent.adapterType === "codex_local" && + transientRecovery?.errorFamily === "transient_upstream" ? resolveCodexTransientFallbackMode(nextAttempt) : null; const transientRetryNotBefore = transientRecovery?.retryNotBefore ?? null; @@ -11279,7 +12900,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) await escalatePlanApprovalResumeFailureNeedsAttention({ run, issueId, - attempt: Math.min(run.scheduledRetryAttempt ?? maxAttempts, maxAttempts), + attempt: Math.min( + run.scheduledRetryAttempt ?? maxAttempts, + maxAttempts, + ), maxAttempts, }).catch((error) => { logger.warn( @@ -11302,7 +12926,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventType: "lifecycle", stream: "system", level: "warn", - message: "Scheduled retry suppressed because the agent is not invokable", + message: + "Scheduled retry suppressed because the agent is not invokable", payload: { retryReason, scheduledRetryAttempt: nextAttempt, @@ -11314,7 +12939,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); return { outcome: "not_scheduled" as const, - reason: "Scheduled retry suppressed because the agent is not invokable", + reason: + "Scheduled retry suppressed because the agent is not invokable", errorCode: "agent_not_invokable" as const, issueId, }; @@ -11322,11 +12948,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const schedule = - transientRetryNotBefore && transientRetryNotBefore.getTime() > baseSchedule.dueAt.getTime() + transientRetryNotBefore && + transientRetryNotBefore.getTime() > baseSchedule.dueAt.getTime() ? { ...baseSchedule, dueAt: transientRetryNotBefore, - delayMs: Math.max(0, transientRetryNotBefore.getTime() - now.getTime()), + delayMs: Math.max( + 0, + transientRetryNotBefore.getTime() - now.getTime(), + ), } : baseSchedule; @@ -11339,7 +12969,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) agent, contextSnapshot, retryReason, - enforceIssueExecutionLock: retryReason === MAX_TURN_CONTINUATION_RETRY_REASON, + enforceIssueExecutionLock: + retryReason === MAX_TURN_CONTINUATION_RETRY_REASON, }); if (!gate.allowed) { await appendRunEvent(run, await nextRunEventSeq(run.id), { @@ -11364,53 +12995,84 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null); const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey); - const interactionContinuationPayload = retryReason === INTERACTION_CONTINUATION_INFRA_RETRY_REASON - ? { - mutation: "interaction", - interactionId: readNonEmptyString(contextSnapshot.interactionId), - interactionKind: readNonEmptyString(contextSnapshot.interactionKind), - interactionStatus: readNonEmptyString(contextSnapshot.interactionStatus), - continuationPolicy: readNonEmptyString(contextSnapshot.continuationPolicy), - } - : {}; + const interactionContinuationPayload = + retryReason === INTERACTION_CONTINUATION_INFRA_RETRY_REASON + ? { + mutation: "interaction", + interactionId: readNonEmptyString(contextSnapshot.interactionId), + interactionKind: readNonEmptyString( + contextSnapshot.interactionKind, + ), + interactionStatus: readNonEmptyString( + contextSnapshot.interactionStatus, + ), + continuationPolicy: readNonEmptyString( + contextSnapshot.continuationPolicy, + ), + } + : {}; const workspaceValidationRetryPayload = - retryReason === INTERACTION_CONTINUATION_INFRA_RETRY_REASON && isWorkspaceValidationFailedRun(run) + retryReason === INTERACTION_CONTINUATION_INFRA_RETRY_REASON && + isWorkspaceValidationFailedRun(run) ? readWorkspaceValidationPayloadFromRun(run) : null; const shouldQuarantineWorkspaceForRetry = workspaceValidationRetryPayload !== null && Object.keys(workspaceValidationRetryPayload).length > 0; - const retryContextSnapshot: Record = withRecoveryModelProfileHint({ - ...contextSnapshot, - retryOfRunId: run.id, - wakeReason, - retryReason, - ...(shouldQuarantineWorkspaceForRetry - ? { - workspaceValidationRecovery: { - strategy: "quarantine_failed_workspace_and_retry_clean", - sourceRunId: run.id, - reason: readNonEmptyString(workspaceValidationRetryPayload?.reason) ?? WORKSPACE_VALIDATION_FAILURE_CODE, - fingerprint: readNonEmptyString(workspaceValidationRetryPayload?.fingerprint), - failedExecutionWorkspaceId: readNonEmptyString(workspaceValidationRetryPayload?.executionWorkspaceId), - }, - } - : {}), - ...(transientRecovery ? { errorFamily: transientRecovery.errorFamily } : {}), - scheduledRetryAttempt: schedule.attempt, - scheduledRetryAt: schedule.dueAt.toISOString(), - ...(transientRetryNotBefore ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } : {}), - ...(transientRecovery?.errorFamily === "provider_quota" && transientRetryNotBefore - ? { providerQuotaRetryNotBefore: transientRetryNotBefore.toISOString() } - : {}), - ...(codexTransientFallbackMode ? { codexTransientFallbackMode } : {}), - }, "normal_model"); - const responsibleUserId = await resolveResponsibleUserIdForRunContext(run, retryContextSnapshot); - const continuationRetryIdempotencyKey = retryReason === MAX_TURN_CONTINUATION_RETRY_REASON - ? `max-turn-continuation:${run.companyId}:${issueId ?? "no-issue"}:${run.id}:${schedule.attempt}` - : retryReason === INTERACTION_CONTINUATION_INFRA_RETRY_REASON - ? `interaction-continuation:${run.companyId}:${issueId ?? "no-issue"}:${run.id}:${schedule.attempt}` - : null; + const retryContextSnapshot: Record = + withRecoveryModelProfileHint( + { + ...contextSnapshot, + retryOfRunId: run.id, + wakeReason, + retryReason, + ...(shouldQuarantineWorkspaceForRetry + ? { + workspaceValidationRecovery: { + strategy: "quarantine_failed_workspace_and_retry_clean", + sourceRunId: run.id, + reason: + readNonEmptyString( + workspaceValidationRetryPayload?.reason, + ) ?? WORKSPACE_VALIDATION_FAILURE_CODE, + fingerprint: readNonEmptyString( + workspaceValidationRetryPayload?.fingerprint, + ), + failedExecutionWorkspaceId: readNonEmptyString( + workspaceValidationRetryPayload?.executionWorkspaceId, + ), + }, + } + : {}), + ...(transientRecovery + ? { errorFamily: transientRecovery.errorFamily } + : {}), + scheduledRetryAttempt: schedule.attempt, + scheduledRetryAt: schedule.dueAt.toISOString(), + ...(transientRetryNotBefore + ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } + : {}), + ...(transientRecovery?.errorFamily === "provider_quota" && + transientRetryNotBefore + ? { + providerQuotaRetryNotBefore: + transientRetryNotBefore.toISOString(), + } + : {}), + ...(codexTransientFallbackMode ? { codexTransientFallbackMode } : {}), + }, + "normal_model", + ); + const responsibleUserId = await resolveResponsibleUserIdForRunContext( + run, + retryContextSnapshot, + ); + const continuationRetryIdempotencyKey = + retryReason === MAX_TURN_CONTINUATION_RETRY_REASON + ? `max-turn-continuation:${run.companyId}:${issueId ?? "no-issue"}:${run.id}:${schedule.attempt}` + : retryReason === INTERACTION_CONTINUATION_INFRA_RETRY_REASON + ? `interaction-continuation:${run.companyId}:${issueId ?? "no-issue"}:${run.id}:${schedule.attempt}` + : null; type ScheduledRetryTransactionResult = | { @@ -11432,362 +13094,437 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) details: Record; }; - const scheduleResult = await db.transaction(async (tx): Promise => { - if (retryReason === INTERACTION_CONTINUATION_INFRA_RETRY_REASON) { - if (issueId) { - await tx.execute( - sql`select id from issues where company_id = ${run.companyId} and id = ${issueId} for update`, - ); - } else { - await tx.execute( - sql`select id from heartbeat_runs where company_id = ${run.companyId} and id = ${run.id} for update`, - ); - } - - const existingContinuation = await tx - .select() - .from(heartbeatRuns) - .where( - and( - eq(heartbeatRuns.companyId, run.companyId), - eq(heartbeatRuns.retryOfRunId, run.id), - eq(heartbeatRuns.scheduledRetryReason, retryReason), - eq(heartbeatRuns.scheduledRetryAttempt, schedule.attempt), - inArray(heartbeatRuns.status, [...MAX_TURN_CONTINUATION_LIVE_RUN_STATUSES]), - issueId - ? sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}` - : sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' is null`, - ), - ) - .orderBy(asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id)) - .limit(1) - .then((rows) => rows[0] ?? null); - - if (existingContinuation) { - if (existingContinuation.wakeupRequestId) { - const existingWakeup = await tx - .select({ coalescedCount: agentWakeupRequests.coalescedCount }) - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, existingContinuation.wakeupRequestId)) - .then((rows) => rows[0] ?? null); - - await tx - .update(agentWakeupRequests) - .set({ - coalescedCount: (existingWakeup?.coalescedCount ?? 0) + 1, - updatedAt: now, - }) - .where(eq(agentWakeupRequests.id, existingContinuation.wakeupRequestId)); + const scheduleResult = await db.transaction( + async (tx): Promise => { + if (retryReason === INTERACTION_CONTINUATION_INFRA_RETRY_REASON) { + if (issueId) { + await tx.execute( + sql`select id from issues where company_id = ${run.companyId} and id = ${issueId} for update`, + ); + } else { + await tx.execute( + sql`select id from heartbeat_runs where company_id = ${run.companyId} and id = ${run.id} for update`, + ); } - return { - outcome: "scheduled", - run: existingContinuation, - reusedExisting: true, - }; - } - } + const existingContinuation = await tx + .select() + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.companyId, run.companyId), + eq(heartbeatRuns.retryOfRunId, run.id), + eq(heartbeatRuns.scheduledRetryReason, retryReason), + eq(heartbeatRuns.scheduledRetryAttempt, schedule.attempt), + inArray(heartbeatRuns.status, [ + ...MAX_TURN_CONTINUATION_LIVE_RUN_STATUSES, + ]), + issueId + ? sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}` + : sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' is null`, + ), + ) + .orderBy(asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id)) + .limit(1) + .then((rows) => rows[0] ?? null); - if (retryReason === MAX_TURN_CONTINUATION_RETRY_REASON) { - if (issueId) { - await tx.execute( - sql`select id from issues where company_id = ${run.companyId} and id = ${issueId} for update`, - ); - } else { - await tx.execute( - sql`select id from heartbeat_runs where company_id = ${run.companyId} and id = ${run.id} for update`, - ); + if (existingContinuation) { + if (existingContinuation.wakeupRequestId) { + const existingWakeup = await tx + .select({ coalescedCount: agentWakeupRequests.coalescedCount }) + .from(agentWakeupRequests) + .where( + eq( + agentWakeupRequests.id, + existingContinuation.wakeupRequestId, + ), + ) + .then((rows) => rows[0] ?? null); + + await tx + .update(agentWakeupRequests) + .set({ + coalescedCount: (existingWakeup?.coalescedCount ?? 0) + 1, + updatedAt: now, + }) + .where( + eq( + agentWakeupRequests.id, + existingContinuation.wakeupRequestId, + ), + ); + } + + return { + outcome: "scheduled", + run: existingContinuation, + reusedExisting: true, + }; + } } - const existingContinuation = await tx - .select() - .from(heartbeatRuns) - .where( - and( - eq(heartbeatRuns.companyId, run.companyId), - eq(heartbeatRuns.retryOfRunId, run.id), - eq(heartbeatRuns.scheduledRetryReason, retryReason), - eq(heartbeatRuns.scheduledRetryAttempt, schedule.attempt), - inArray(heartbeatRuns.status, [...MAX_TURN_CONTINUATION_LIVE_RUN_STATUSES]), - issueId - ? sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}` - : sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' is null`, - ), - ) - .orderBy(asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id)) - .limit(1) - .then((rows) => rows[0] ?? null); - - if (existingContinuation) { - if (existingContinuation.wakeupRequestId) { - const existingWakeup = await tx - .select({ coalescedCount: agentWakeupRequests.coalescedCount }) - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, existingContinuation.wakeupRequestId)) - .then((rows) => rows[0] ?? null); - - await tx - .update(agentWakeupRequests) - .set({ - coalescedCount: (existingWakeup?.coalescedCount ?? 0) + 1, - updatedAt: now, - }) - .where(eq(agentWakeupRequests.id, existingContinuation.wakeupRequestId)); + if (retryReason === MAX_TURN_CONTINUATION_RETRY_REASON) { + if (issueId) { + await tx.execute( + sql`select id from issues where company_id = ${run.companyId} and id = ${issueId} for update`, + ); + } else { + await tx.execute( + sql`select id from heartbeat_runs where company_id = ${run.companyId} and id = ${run.id} for update`, + ); } - return { - outcome: "scheduled", - run: existingContinuation, - reusedExisting: true, - }; + const existingContinuation = await tx + .select() + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.companyId, run.companyId), + eq(heartbeatRuns.retryOfRunId, run.id), + eq(heartbeatRuns.scheduledRetryReason, retryReason), + eq(heartbeatRuns.scheduledRetryAttempt, schedule.attempt), + inArray(heartbeatRuns.status, [ + ...MAX_TURN_CONTINUATION_LIVE_RUN_STATUSES, + ]), + issueId + ? sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}` + : sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' is null`, + ), + ) + .orderBy(asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id)) + .limit(1) + .then((rows) => rows[0] ?? null); + + if (existingContinuation) { + if (existingContinuation.wakeupRequestId) { + const existingWakeup = await tx + .select({ coalescedCount: agentWakeupRequests.coalescedCount }) + .from(agentWakeupRequests) + .where( + eq( + agentWakeupRequests.id, + existingContinuation.wakeupRequestId, + ), + ) + .then((rows) => rows[0] ?? null); + + await tx + .update(agentWakeupRequests) + .set({ + coalescedCount: (existingWakeup?.coalescedCount ?? 0) + 1, + updatedAt: now, + }) + .where( + eq( + agentWakeupRequests.id, + existingContinuation.wakeupRequestId, + ), + ); + } + + return { + outcome: "scheduled", + run: existingContinuation, + reusedExisting: true, + }; + } + + if (issueId) { + const lockedIssue = await tx + .select({ + id: issues.id, + status: issues.status, + assigneeAgentId: issues.assigneeAgentId, + executionRunId: issues.executionRunId, + }) + .from(issues) + .where( + and( + eq(issues.id, issueId), + eq(issues.companyId, run.companyId), + ), + ) + .then((rows) => rows[0] ?? null); + + if (!lockedIssue) { + return { + outcome: "not_scheduled", + reason: + "Scheduled max-turn continuation suppressed because the target issue no longer exists", + errorCode: "issue_not_found", + issueId, + details: { issueId }, + }; + } + + if (lockedIssue.assigneeAgentId !== run.agentId) { + return { + outcome: "not_scheduled", + reason: + "Scheduled max-turn continuation suppressed because issue ownership changed", + errorCode: "issue_reassigned", + issueId, + details: { + issueId, + previousAssigneeAgentId: run.agentId, + currentAssigneeAgentId: lockedIssue.assigneeAgentId, + }, + }; + } + + if ( + lockedIssue.status === "cancelled" || + lockedIssue.status === "done" + ) { + return { + outcome: "not_scheduled", + reason: `Scheduled max-turn continuation suppressed because issue reached terminal status (${lockedIssue.status})`, + errorCode: + lockedIssue.status === "cancelled" + ? "issue_cancelled" + : "issue_terminal_status", + issueId, + details: { issueId, currentStatus: lockedIssue.status }, + }; + } + + if (lockedIssue.status !== "in_progress") { + return { + outcome: "not_scheduled", + reason: `Scheduled max-turn continuation suppressed because issue is no longer in_progress (current status: ${lockedIssue.status})`, + errorCode: "issue_not_in_progress", + issueId, + details: { + issueId, + currentStatus: lockedIssue.status, + requiredStatus: "in_progress", + }, + }; + } + + if (lockedIssue.executionRunId !== run.id) { + return { + outcome: "not_scheduled", + reason: + "Scheduled max-turn continuation suppressed because the issue execution lock belongs to a different run", + errorCode: "issue_execution_lock_changed", + issueId, + details: { + issueId, + expectedExecutionRunId: run.id, + currentExecutionRunId: lockedIssue.executionRunId, + }, + }; + } + } } - if (issueId) { - const lockedIssue = await tx + const wakeupRequest = await tx + .insert(agentWakeupRequests) + .values({ + companyId: run.companyId, + agentId: run.agentId, + source: "automation", + triggerDetail: "system", + reason: wakeReason, + payload: withRecoveryModelProfileHint( + { + ...(issueId ? { issueId } : {}), + retryOfRunId: run.id, + ...interactionContinuationPayload, + retryReason, + ...(transientRecovery + ? { errorFamily: transientRecovery.errorFamily } + : {}), + scheduledRetryAttempt: schedule.attempt, + scheduledRetryAt: schedule.dueAt.toISOString(), + ...(transientRetryNotBefore + ? { + transientRetryNotBefore: + transientRetryNotBefore.toISOString(), + } + : {}), + ...(transientRecovery?.errorFamily === "provider_quota" && + transientRetryNotBefore + ? { + providerQuotaRetryNotBefore: + transientRetryNotBefore.toISOString(), + } + : {}), + ...(codexTransientFallbackMode + ? { codexTransientFallbackMode } + : {}), + }, + "normal_model", + ), + status: "queued", + requestedByActorType: "system", + requestedByActorId: null, + idempotencyKey: continuationRetryIdempotencyKey, + updatedAt: now, + }) + .returning() + .then((rows) => rows[0]); + + const scheduledRun = await tx + .insert(heartbeatRuns) + .values({ + companyId: run.companyId, + agentId: run.agentId, + invocationSource: "automation", + triggerDetail: "system", + status: "scheduled_retry", + wakeupRequestId: wakeupRequest.id, + contextSnapshot: retryContextSnapshot, + responsibleUserId, + sessionIdBefore: sessionBefore, + retryOfRunId: run.id, + scheduledRetryAt: schedule.dueAt, + scheduledRetryAttempt: schedule.attempt, + scheduledRetryReason: retryReason, + continuationAttempt: readContinuationAttempt( + retryContextSnapshot.livenessContinuationAttempt, + ), + updatedAt: now, + }) + .returning() + .then((rows) => rows[0]); + + await tx + .update(agentWakeupRequests) + .set({ + runId: scheduledRun.id, + updatedAt: now, + }) + .where(eq(agentWakeupRequests.id, wakeupRequest.id)); + + let detachWorkspaceFromIssue = false; + if (issueId && shouldQuarantineWorkspaceForRetry) { + const issueWorkspace = await tx .select({ id: issues.id, - status: issues.status, - assigneeAgentId: issues.assigneeAgentId, - executionRunId: issues.executionRunId, + companyId: issues.companyId, + executionWorkspaceId: issues.executionWorkspaceId, }) .from(issues) - .where(and(eq(issues.id, issueId), eq(issues.companyId, run.companyId))) - .then((rows) => rows[0] ?? null); - - if (!lockedIssue) { - return { - outcome: "not_scheduled", - reason: "Scheduled max-turn continuation suppressed because the target issue no longer exists", - errorCode: "issue_not_found", - issueId, - details: { issueId }, - }; - } - - if (lockedIssue.assigneeAgentId !== run.agentId) { - return { - outcome: "not_scheduled", - reason: "Scheduled max-turn continuation suppressed because issue ownership changed", - errorCode: "issue_reassigned", - issueId, - details: { - issueId, - previousAssigneeAgentId: run.agentId, - currentAssigneeAgentId: lockedIssue.assigneeAgentId, - }, - }; - } - - if (lockedIssue.status === "cancelled" || lockedIssue.status === "done") { - return { - outcome: "not_scheduled", - reason: `Scheduled max-turn continuation suppressed because issue reached terminal status (${lockedIssue.status})`, - errorCode: lockedIssue.status === "cancelled" ? "issue_cancelled" : "issue_terminal_status", - issueId, - details: { issueId, currentStatus: lockedIssue.status }, - }; - } - - if (lockedIssue.status !== "in_progress") { - return { - outcome: "not_scheduled", - reason: `Scheduled max-turn continuation suppressed because issue is no longer in_progress (current status: ${lockedIssue.status})`, - errorCode: "issue_not_in_progress", - issueId, - details: { issueId, currentStatus: lockedIssue.status, requiredStatus: "in_progress" }, - }; - } - - if (lockedIssue.executionRunId !== run.id) { - return { - outcome: "not_scheduled", - reason: - "Scheduled max-turn continuation suppressed because the issue execution lock belongs to a different run", - errorCode: "issue_execution_lock_changed", - issueId, - details: { - issueId, - expectedExecutionRunId: run.id, - currentExecutionRunId: lockedIssue.executionRunId, - }, - }; - } - } - } - - const wakeupRequest = await tx - .insert(agentWakeupRequests) - .values({ - companyId: run.companyId, - agentId: run.agentId, - source: "automation", - triggerDetail: "system", - reason: wakeReason, - payload: withRecoveryModelProfileHint({ - ...(issueId ? { issueId } : {}), - retryOfRunId: run.id, - ...interactionContinuationPayload, - retryReason, - ...(transientRecovery ? { errorFamily: transientRecovery.errorFamily } : {}), - scheduledRetryAttempt: schedule.attempt, - scheduledRetryAt: schedule.dueAt.toISOString(), - ...(transientRetryNotBefore ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } : {}), - ...(transientRecovery?.errorFamily === "provider_quota" && transientRetryNotBefore - ? { providerQuotaRetryNotBefore: transientRetryNotBefore.toISOString() } - : {}), - ...(codexTransientFallbackMode ? { codexTransientFallbackMode } : {}), - }, "normal_model"), - status: "queued", - requestedByActorType: "system", - requestedByActorId: null, - idempotencyKey: continuationRetryIdempotencyKey, - updatedAt: now, - }) - .returning() - .then((rows) => rows[0]); - - const scheduledRun = await tx - .insert(heartbeatRuns) - .values({ - companyId: run.companyId, - agentId: run.agentId, - invocationSource: "automation", - triggerDetail: "system", - status: "scheduled_retry", - wakeupRequestId: wakeupRequest.id, - contextSnapshot: retryContextSnapshot, - responsibleUserId, - sessionIdBefore: sessionBefore, - retryOfRunId: run.id, - scheduledRetryAt: schedule.dueAt, - scheduledRetryAttempt: schedule.attempt, - scheduledRetryReason: retryReason, - continuationAttempt: readContinuationAttempt(retryContextSnapshot.livenessContinuationAttempt), - updatedAt: now, - }) - .returning() - .then((rows) => rows[0]); - - await tx - .update(agentWakeupRequests) - .set({ - runId: scheduledRun.id, - updatedAt: now, - }) - .where(eq(agentWakeupRequests.id, wakeupRequest.id)); - - let detachWorkspaceFromIssue = false; - if (issueId && shouldQuarantineWorkspaceForRetry) { - const issueWorkspace = await tx - .select({ - id: issues.id, - companyId: issues.companyId, - executionWorkspaceId: issues.executionWorkspaceId, - }) - .from(issues) - .where(and(eq(issues.id, issueId), eq(issues.companyId, run.companyId))) - .for("update") - .then((rows) => rows[0] ?? null); - const failedExecutionWorkspaceId = - readNonEmptyString(workspaceValidationRetryPayload?.executionWorkspaceId) ?? - readNonEmptyString(issueWorkspace?.executionWorkspaceId); - - if (issueWorkspace && failedExecutionWorkspaceId) { - const failedWorkspace = await tx - .select({ - id: executionWorkspaces.id, - companyId: executionWorkspaces.companyId, - sourceIssueId: executionWorkspaces.sourceIssueId, - status: executionWorkspaces.status, - metadata: executionWorkspaces.metadata, - }) - .from(executionWorkspaces) - .where(and( - eq(executionWorkspaces.id, failedExecutionWorkspaceId), - eq(executionWorkspaces.companyId, run.companyId), - )) + .where( + and(eq(issues.id, issueId), eq(issues.companyId, run.companyId)), + ) .for("update") .then((rows) => rows[0] ?? null); + const failedExecutionWorkspaceId = + readNonEmptyString( + workspaceValidationRetryPayload?.executionWorkspaceId, + ) ?? readNonEmptyString(issueWorkspace?.executionWorkspaceId); - const workspaceBelongsToIssue = - failedWorkspace + if (issueWorkspace && failedExecutionWorkspaceId) { + const failedWorkspace = await tx + .select({ + id: executionWorkspaces.id, + companyId: executionWorkspaces.companyId, + sourceIssueId: executionWorkspaces.sourceIssueId, + status: executionWorkspaces.status, + metadata: executionWorkspaces.metadata, + }) + .from(executionWorkspaces) + .where( + and( + eq(executionWorkspaces.id, failedExecutionWorkspaceId), + eq(executionWorkspaces.companyId, run.companyId), + ), + ) + .for("update") + .then((rows) => rows[0] ?? null); + + const workspaceBelongsToIssue = failedWorkspace ? failedWorkspace.sourceIssueId === issueId : false; - if ( - failedWorkspace && - workspaceBelongsToIssue && - issueWorkspace.executionWorkspaceId === failedExecutionWorkspaceId - ) { - const existingMetadata = parseObject(failedWorkspace.metadata); - const quarantine = { - reason: WORKSPACE_VALIDATION_FAILURE_CODE, - retryReason, - sourceRunId: run.id, - retryRunId: scheduledRun.id, - issueId, - sourceIssueId: failedWorkspace.sourceIssueId ?? null, - quarantinedAt: now.toISOString(), - workspaceValidation: workspaceValidationRetryPayload ?? {}, - }; - await tx - .update(executionWorkspaces) - .set({ - status: "archived", - closedAt: now, - cleanupEligibleAt: null, - cleanupReason: WORKSPACE_VALIDATION_FAILURE_CODE, - metadata: { - ...existingMetadata, - workspaceValidationQuarantine: quarantine, - }, - updatedAt: now, - }) - .where(and( - eq(executionWorkspaces.id, failedWorkspace.id), - eq(executionWorkspaces.companyId, run.companyId), - )); + if ( + failedWorkspace && + workspaceBelongsToIssue && + issueWorkspace.executionWorkspaceId === failedExecutionWorkspaceId + ) { + const existingMetadata = parseObject(failedWorkspace.metadata); + const quarantine = { + reason: WORKSPACE_VALIDATION_FAILURE_CODE, + retryReason, + sourceRunId: run.id, + retryRunId: scheduledRun.id, + issueId, + sourceIssueId: failedWorkspace.sourceIssueId ?? null, + quarantinedAt: now.toISOString(), + workspaceValidation: workspaceValidationRetryPayload ?? {}, + }; + await tx + .update(executionWorkspaces) + .set({ + status: "archived", + closedAt: now, + cleanupEligibleAt: null, + cleanupReason: WORKSPACE_VALIDATION_FAILURE_CODE, + metadata: { + ...existingMetadata, + workspaceValidationQuarantine: quarantine, + }, + updatedAt: now, + }) + .where( + and( + eq(executionWorkspaces.id, failedWorkspace.id), + eq(executionWorkspaces.companyId, run.companyId), + ), + ); - await logActivity(tx as unknown as Db, { - companyId: run.companyId, - actorType: "system", - actorId: "heartbeat", - agentId: run.agentId, - runId: run.id, - action: "execution_workspace.workspace_validation_quarantined", - entityType: "execution_workspace", - entityId: failedWorkspace.id, - details: quarantine, - }); - detachWorkspaceFromIssue = issueWorkspace.executionWorkspaceId === failedExecutionWorkspaceId; + await logActivity(tx as unknown as Db, { + companyId: run.companyId, + actorType: "system", + actorId: "heartbeat", + agentId: run.agentId, + runId: run.id, + action: "execution_workspace.workspace_validation_quarantined", + entityType: "execution_workspace", + entityId: failedWorkspace.id, + details: quarantine, + }); + detachWorkspaceFromIssue = + issueWorkspace.executionWorkspaceId === + failedExecutionWorkspaceId; + } } } - } - if (issueId) { - await tx - .update(issues) - .set({ - executionRunId: scheduledRun.id, - executionAgentNameKey: normalizeAgentNameKey(agent.name), - executionLockedAt: now, - ...(detachWorkspaceFromIssue - ? { - executionWorkspaceId: null, - executionWorkspacePreference: null, - } - : {}), - updatedAt: now, - }) - .where(and(eq(issues.id, issueId), eq(issues.companyId, run.companyId), eq(issues.executionRunId, run.id))); - } + if (issueId) { + await tx + .update(issues) + .set({ + executionRunId: scheduledRun.id, + executionAgentNameKey: normalizeAgentNameKey(agent.name), + executionLockedAt: now, + ...(detachWorkspaceFromIssue + ? { + executionWorkspaceId: null, + executionWorkspacePreference: null, + } + : {}), + updatedAt: now, + }) + .where( + and( + eq(issues.id, issueId), + eq(issues.companyId, run.companyId), + eq(issues.executionRunId, run.id), + ), + ); + } - return { - outcome: "scheduled", - run: scheduledRun, - reusedExisting: false, - }; - }); + return { + outcome: "scheduled", + run: scheduledRun, + reusedExisting: false, + }; + }, + ); if (scheduleResult.outcome === "not_scheduled") { await appendRunEvent(run, await nextRunEventSeq(run.id), { @@ -11811,7 +13548,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const retryRun = scheduleResult.run; - const dueAt = retryRun.scheduledRetryAt ? new Date(retryRun.scheduledRetryAt) : schedule.dueAt; + const dueAt = retryRun.scheduledRetryAt + ? new Date(retryRun.scheduledRetryAt) + : schedule.dueAt; if (scheduleResult.reusedExisting) { await appendRunEvent(run, await nextRunEventSeq(run.id), { @@ -11846,14 +13585,22 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) payload: { retryRunId: retryRun.id, retryReason, - ...(transientRecovery ? { errorFamily: transientRecovery.errorFamily } : {}), + ...(transientRecovery + ? { errorFamily: transientRecovery.errorFamily } + : {}), scheduledRetryAttempt: schedule.attempt, scheduledRetryAt: schedule.dueAt.toISOString(), baseDelayMs: schedule.baseDelayMs, delayMs: schedule.delayMs, - ...(transientRetryNotBefore ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } : {}), - ...(transientRecovery?.errorFamily === "provider_quota" && transientRetryNotBefore - ? { providerQuotaRetryNotBefore: transientRetryNotBefore.toISOString() } + ...(transientRetryNotBefore + ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } + : {}), + ...(transientRecovery?.errorFamily === "provider_quota" && + transientRetryNotBefore + ? { + providerQuotaRetryNotBefore: + transientRetryNotBefore.toISOString(), + } : {}), ...(codexTransientFallbackMode ? { codexTransientFallbackMode } : {}), }, @@ -11903,7 +13650,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) now?: Date; }): Promise { const staleCutoff = new Date( - (input.now ?? new Date()).getTime() - WORKSPACE_BUSY_HOLDER_STALE_AFTER_MS, + (input.now ?? new Date()).getTime() - + WORKSPACE_BUSY_HOLDER_STALE_AFTER_MS, ); return await db .select({ @@ -11997,19 +13745,24 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) error: deferral.message, }).catch(() => undefined); - const cancelledRun = cancelWrite.run ?? (await getRun(run.id).catch(() => null)); + const cancelledRun = + cancelWrite.run ?? (await getRun(run.id).catch(() => null)); const agentRow = await getAgent(run.agentId).catch(() => null); let scheduleOutcome: string | null = null; if (cancelledRun && agentRow) { - const scheduleResult = await scheduleBoundedRetryForRun(cancelledRun, agentRow, { - now, - retryReason: WORKSPACE_BUSY_RETRY_REASON, - wakeReason: WORKSPACE_BUSY_RETRY_WAKE_REASON, - // Always admit the next attempt: workspace-busy deferral is bounded by - // holder liveness, not by an attempt counter. - maxAttempts: (cancelledRun.scheduledRetryAttempt ?? 0) + 1, - delayMs: computeWorkspaceBusyRetryDelayMs(), - }).catch((scheduleErr) => { + const scheduleResult = await scheduleBoundedRetryForRun( + cancelledRun, + agentRow, + { + now, + retryReason: WORKSPACE_BUSY_RETRY_REASON, + wakeReason: WORKSPACE_BUSY_RETRY_WAKE_REASON, + // Always admit the next attempt: workspace-busy deferral is bounded by + // holder liveness, not by an attempt counter. + maxAttempts: (cancelledRun.scheduledRetryAttempt ?? 0) + 1, + delayMs: computeWorkspaceBusyRetryDelayMs(), + }, + ).catch((scheduleErr) => { logger.error( { err: scheduleErr, runId: run.id }, "failed to schedule workspace-busy retry after deferral", @@ -12020,31 +13773,37 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } if (cancelledRun) { - await appendRunEvent(cancelledRun, await nextRunEventSeq(cancelledRun.id), { - eventType: "lifecycle", - stream: "system", - level: "info", - message: - scheduleOutcome === "scheduled" - ? `Deferred: ${deferral.message}. Retry ${deferral.deferralAttempt + 1} scheduled; the run waits for the workspace to free.` - : `Deferred: ${deferral.message}. No retry could be scheduled; releasing the issue for other runs.`, - payload: { - projectWorkspaceId: deferral.projectWorkspaceId, - holderRunId: deferral.holder.runId, - holderIssueId: deferral.holder.issueId, - deferralAttempt: deferral.deferralAttempt, - retryScheduled: scheduleOutcome === "scheduled", + await appendRunEvent( + cancelledRun, + await nextRunEventSeq(cancelledRun.id), + { + eventType: "lifecycle", + stream: "system", + level: "info", + message: + scheduleOutcome === "scheduled" + ? `Deferred: ${deferral.message}. Retry ${deferral.deferralAttempt + 1} scheduled; the run waits for the workspace to free.` + : `Deferred: ${deferral.message}. No retry could be scheduled; releasing the issue for other runs.`, + payload: { + projectWorkspaceId: deferral.projectWorkspaceId, + holderRunId: deferral.holder.runId, + holderIssueId: deferral.holder.issueId, + deferralAttempt: deferral.deferralAttempt, + retryScheduled: scheduleOutcome === "scheduled", + }, }, - }).catch(() => undefined); + ).catch(() => undefined); } if (cancelledRun && scheduleOutcome !== "scheduled") { - await releaseIssueExecutionAndPromote(cancelledRun).catch((releaseErr) => { - logger.error( - { err: releaseErr, runId: run.id }, - "failed to release issue execution after workspace-busy deferral", - ); - }); + await releaseIssueExecutionAndPromote(cancelledRun).catch( + (releaseErr) => { + logger.error( + { err: releaseErr, runId: run.id }, + "failed to release issue execution after workspace-busy deferral", + ); + }, + ); } await finalizeAgentStatus(run.agentId, "cancelled", null, { @@ -12057,14 +13816,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) agent: typeof agents.$inferSelect, ) { if (!run.wakeupRequestId) return null; - if (!isResolvedInteractionContinuationWakeContext(run.contextSnapshot)) return null; + if (!isResolvedInteractionContinuationWakeContext(run.contextSnapshot)) + return null; if (!isRetryableInteractionContinuationInfrastructureFailure(run)) { const context = parseObject(run.contextSnapshot); const issueId = readNonEmptyString(context.issueId); await escalatePlanApprovalResumeFailureNeedsAttention({ run, issueId, - attempt: Math.min(run.scheduledRetryAttempt ?? INTERACTION_CONTINUATION_INFRA_MAX_ATTEMPTS, INTERACTION_CONTINUATION_INFRA_MAX_ATTEMPTS), + attempt: Math.min( + run.scheduledRetryAttempt ?? + INTERACTION_CONTINUATION_INFRA_MAX_ATTEMPTS, + INTERACTION_CONTINUATION_INFRA_MAX_ATTEMPTS, + ), maxAttempts: INTERACTION_CONTINUATION_INFRA_MAX_ATTEMPTS, }).catch((error) => { logger.warn( @@ -12094,7 +13858,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) cutoff ? gte(heartbeatRuns.createdAt, cutoff) : undefined, ), ) - .orderBy(asc(heartbeatRuns.scheduledRetryAt), asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id)) + .orderBy( + asc(heartbeatRuns.scheduledRetryAt), + asc(heartbeatRuns.createdAt), + asc(heartbeatRuns.id), + ) .limit(50); const promotedRunIds: string[] = []; @@ -12133,17 +13901,23 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) sql`${heartbeatRuns.retryOfRunId} is not null`, ), ) - .orderBy(desc(heartbeatRuns.updatedAt), desc(heartbeatRuns.createdAt), desc(heartbeatRuns.id)) + .orderBy( + desc(heartbeatRuns.updatedAt), + desc(heartbeatRuns.createdAt), + desc(heartbeatRuns.id), + ) .limit(1) .then((rows) => rows[0] ?? null); } - function summarizeIssueScheduledRetryRun( - row: { run: typeof heartbeatRuns.$inferSelect; agentName: string | null }, - ) { + function summarizeIssueScheduledRetryRun(row: { + run: typeof heartbeatRuns.$inferSelect; + agentName: string | null; + }) { return { runId: row.run.id, - status: row.run.status as "scheduled_retry" | "queued" | "running" | "cancelled", + status: row.run.status as + "scheduled_retry" | "queued" | "running" | "cancelled", agentId: row.run.agentId, agentName: row.agentName, retryOfRunId: row.run.retryOfRunId, @@ -12157,7 +13931,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) async function retryScheduledRetryNow(input: { issueId: string; - actor?: { actorType?: "user" | "agent" | "system"; actorId?: string | null }; + actor?: { + actorType?: "user" | "agent" | "system"; + actorId?: string | null; + }; now?: Date; }) { const now = input.now ?? new Date(); @@ -12168,9 +13945,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .then((rows) => rows[0] ?? null); if (!issue) throw notFound("Issue not found"); - const scheduled = await getIssueRetryRun(issue.companyId, issue.id, ["scheduled_retry"]); + const scheduled = await getIssueRetryRun(issue.companyId, issue.id, [ + "scheduled_retry", + ]); if (!scheduled) { - const alreadyPromoted = await getIssueRetryRun(issue.companyId, issue.id, ["queued", "running"]); + const alreadyPromoted = await getIssueRetryRun( + issue.companyId, + issue.id, + ["queued", "running"], + ); if (alreadyPromoted) { return { outcome: "already_promoted" as const, @@ -12201,20 +13984,25 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) contextSnapshot, updatedAt: now, }) - .where(and(eq(heartbeatRuns.id, scheduled.run.id), eq(heartbeatRuns.status, "scheduled_retry"))) + .where( + and( + eq(heartbeatRuns.id, scheduled.run.id), + eq(heartbeatRuns.status, "scheduled_retry"), + ), + ) .returning() .then((rows) => rows[0] ?? null); if (!row) return null; if (row.wakeupRequestId) { const wakeupPayload = { - ...(parseObject( + ...parseObject( await tx .select({ payload: agentWakeupRequests.payload }) .from(agentWakeupRequests) .where(eq(agentWakeupRequests.id, row.wakeupRequestId)) .then((rows) => rows[0]?.payload ?? null), - )), + ), scheduledRetryAt: now.toISOString(), retryNowRequestedAt: now.toISOString(), }; @@ -12231,7 +14019,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); if (!updated) { - const alreadyPromoted = await getIssueRetryRun(issue.companyId, issue.id, ["queued", "running"]); + const alreadyPromoted = await getIssueRetryRun( + issue.companyId, + issue.id, + ["queued", "running"], + ); if (alreadyPromoted) { return { outcome: "already_promoted" as const, @@ -12254,7 +14046,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) payload: { issueId: issue.id, scheduledRetryAttempt: updated.scheduledRetryAttempt, - scheduledRetryAt: updated.scheduledRetryAt ? new Date(updated.scheduledRetryAt).toISOString() : null, + scheduledRetryAt: updated.scheduledRetryAt + ? new Date(updated.scheduledRetryAt).toISOString() + : null, scheduledRetryReason: updated.scheduledRetryReason, requestedByActorType: input.actor?.actorType ?? null, requestedByActorId: input.actor?.actorId ?? null, @@ -12262,10 +14056,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); const promotion = await promoteScheduledRetryRun(updated, now); - const promotedRow = await getIssueRetryRun(issue.companyId, issue.id, ["queued", "running", "cancelled"]); + const promotedRow = await getIssueRetryRun(issue.companyId, issue.id, [ + "queued", + "running", + "cancelled", + ]); const scheduledRetry = promotedRow ? summarizeIssueScheduledRetryRun(promotedRow) - : summarizeIssueScheduledRetryRun({ run: promotion.run ?? updated, agentName: scheduled.agentName }); + : summarizeIssueScheduledRetryRun({ + run: promotion.run ?? updated, + agentName: scheduled.agentName, + }); if (promotion.outcome === "promoted") { return { @@ -12295,8 +14096,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { enabled: asBoolean(heartbeat.enabled, false), intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)), - wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true), - maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns), + wakeOnDemand: asBoolean( + heartbeat.wakeOnDemand ?? + heartbeat.wakeOnAssignment ?? + heartbeat.wakeOnOnDemand ?? + heartbeat.wakeOnAutomation, + true, + ), + maxConcurrentRuns: normalizeMaxConcurrentRuns( + heartbeat.maxConcurrentRuns, + ), skipTimerWhenNoActionableWork: asBoolean( heartbeat.skipTimerWhenNoActionableWork ?? heartbeat.requireActionableTimerWork ?? @@ -12304,7 +14113,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) false, ), maxDailyRuns: normalizeOptionalNonNegativeInteger( - heartbeat.maxDailyRuns ?? heartbeat.dailyRunLimit ?? heartbeat.dailyRunCap ?? heartbeat.maxRunsPerDay, + heartbeat.maxDailyRuns ?? + heartbeat.dailyRunLimit ?? + heartbeat.dailyRunCap ?? + heartbeat.maxRunsPerDay, ), maxDailyCostCents: normalizeOptionalNonNegativeInteger( heartbeat.maxDailyCostCents ?? @@ -12322,15 +14134,39 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } function currentUtcDayWindow(now = new Date()) { - const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0)); - const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 0, 0, 0, 0)); + const start = new Date( + Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate(), + 0, + 0, + 0, + 0, + ), + ); + const end = new Date( + Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + 1, + 0, + 0, + 0, + 0, + ), + ); return { start, end }; } async function getHeartbeatDailyCapBlock( agent: typeof agents.$inferSelect, policy: ReturnType, - options: { checkRunCap?: boolean; checkCostCap?: boolean; excludeRunId?: string | null } = {}, + options: { + checkRunCap?: boolean; + checkCostCap?: boolean; + excludeRunId?: string | null; + } = {}, client: Pick = db, ) { const checkRunCap = options.checkRunCap ?? true; @@ -12363,7 +14199,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (checkCostCap && policy.maxDailyCostCents !== null) { const [row] = await client - .select({ total: sql`coalesce(sum(${costEvents.costCents})::bigint, 0)` }) + .select({ + total: sql`coalesce(sum(${costEvents.costCents})::bigint, 0)`, + }) .from(costEvents) .where( and( @@ -12388,10 +14226,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) async function cancelQueuedRunForHeartbeatDailyCap( run: typeof heartbeatRuns.$inferSelect, - dailyCapBlock: NonNullable>>, + dailyCapBlock: NonNullable< + Awaited> + >, ) { const now = new Date(); - const reason = "Cancelled because the agent reached a per-day heartbeat budget cap before adapter invocation"; + const reason = + "Cancelled because the agent reached a per-day heartbeat budget cap before adapter invocation"; const cancelled = await setRunStatus(run.id, "cancelled", { finishedAt: now, error: reason, @@ -12426,7 +14267,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }, }); - await releaseIssueExecutionAndPromote(cancelled, { suppressImmediateRecovery: true }); + await releaseIssueExecutionAndPromote(cancelled, { + suppressImmediateRecovery: true, + }); return cancelled; } @@ -12449,7 +14292,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return Boolean(row); } - async function markTimerHeartbeatChecked(agentId: string, source: WakeupOptions["source"]) { + async function markTimerHeartbeatChecked( + agentId: string, + source: WakeupOptions["source"], + ) { if (source !== "timer") return; await db .update(agents) @@ -12472,14 +14318,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) lastHeartbeatAt: now, updatedAt: now, }) - .where(and( - eq(agents.id, agent.id), - eq(agents.companyId, agent.companyId), - or( - lte(agents.lastHeartbeatAt, dueBefore), - and(isNull(agents.lastHeartbeatAt), lte(agents.createdAt, dueBefore)), + .where( + and( + eq(agents.id, agent.id), + eq(agents.companyId, agent.companyId), + or( + lte(agents.lastHeartbeatAt, dueBefore), + and( + isNull(agents.lastHeartbeatAt), + lte(agents.createdAt, dueBefore), + ), + ), ), - )) + ) .returning({ id: agents.id }) .then((rows) => rows[0] ?? null); if (!claimed) return null; @@ -12494,17 +14345,32 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) : undefined; } - function parseMaxTurnContinuationPolicy(agent: typeof agents.$inferSelect): MaxTurnContinuationPolicy { + function parseMaxTurnContinuationPolicy( + agent: typeof agents.$inferSelect, + ): MaxTurnContinuationPolicy { const runtimeConfig = parseObject(agent.runtimeConfig); const heartbeat = parseObject(runtimeConfig.heartbeat); const configured = parseObject(heartbeat.maxTurnContinuation); - const rawMaxAttempts = Math.floor(asNumber(configured.maxAttempts, MAX_TURN_CONTINUATION_DEFAULT_MAX_ATTEMPTS)); - const rawDelayMs = Math.floor(asNumber(configured.delayMs, MAX_TURN_CONTINUATION_DEFAULT_DELAY_MS)); + const rawMaxAttempts = Math.floor( + asNumber( + configured.maxAttempts, + MAX_TURN_CONTINUATION_DEFAULT_MAX_ATTEMPTS, + ), + ); + const rawDelayMs = Math.floor( + asNumber(configured.delayMs, MAX_TURN_CONTINUATION_DEFAULT_DELAY_MS), + ); return { enabled: asBoolean(configured.enabled, true), - maxAttempts: Math.max(0, Math.min(MAX_TURN_CONTINUATION_MAX_ATTEMPTS_CAP, rawMaxAttempts)), - delayMs: Math.max(0, Math.min(MAX_TURN_CONTINUATION_MAX_DELAY_MS, rawDelayMs)), + maxAttempts: Math.max( + 0, + Math.min(MAX_TURN_CONTINUATION_MAX_ATTEMPTS_CAP, rawMaxAttempts), + ), + delayMs: Math.max( + 0, + Math.min(MAX_TURN_CONTINUATION_MAX_DELAY_MS, rawDelayMs), + ), }; } @@ -12527,13 +14393,20 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) companyId: string, queuedRuns: Array, ) { - const issueIds = [...new Set( - queuedRuns - .map((run) => readNonEmptyString(parseObject(run.contextSnapshot).issueId)) - .filter((issueId): issueId is string => Boolean(issueId)), - )]; + const issueIds = [ + ...new Set( + queuedRuns + .map((run) => + readNonEmptyString(parseObject(run.contextSnapshot).issueId), + ) + .filter((issueId): issueId is string => Boolean(issueId)), + ), + ]; if (issueIds.length === 0) { - return new Map>>(); + return new Map< + string, + Awaited> + >(); } return issuesSvc.listDependencyReadiness(companyId, issueIds); } @@ -12542,40 +14415,62 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const [{ count }] = await db .select({ count: sql`count(*)` }) .from(heartbeatRuns) - .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "running"))); + .where( + and( + eq(heartbeatRuns.agentId, agentId), + eq(heartbeatRuns.status, "running"), + ), + ); return Number(count ?? 0); } - async function claimQueuedRun(run: typeof heartbeatRuns.$inferSelect, companyAgents?: AgentOrgRow[]) { + async function claimQueuedRun( + run: typeof heartbeatRuns.$inferSelect, + companyAgents?: AgentOrgRow[], + ) { if (run.status !== "queued") return run; const agent = await getAgent(run.agentId); if (!agent) { - await cancelRunInternal(run.id, "Cancelled because the agent no longer exists"); + await cancelRunInternal( + run.id, + "Cancelled because the agent no longer exists", + ); return null; } const invokability = companyAgents ? evaluateAgentInvokability(toAgentOrgRow(agent), companyAgents) : await getAgentInvokability(agent); if (!invokability.invokable) { - await cancelRunInternal(run.id, `Cancelled because the agent is not invokable: ${invokability.reason}`); + await cancelRunInternal( + run.id, + `Cancelled because the agent is not invokable: ${invokability.reason}`, + ); return null; } const context = parseObject(run.contextSnapshot); - const budgetBlock = await budgets.getInvocationBlock(run.companyId, run.agentId, { - issueId: readNonEmptyString(context.issueId), - projectId: readNonEmptyString(context.projectId), - }); + const budgetBlock = await budgets.getInvocationBlock( + run.companyId, + run.agentId, + { + issueId: readNonEmptyString(context.issueId), + projectId: readNonEmptyString(context.projectId), + }, + ); if (budgetBlock) { await cancelRunInternal(run.id, budgetBlock.reason); return null; } - const dailyCapBlock = await getHeartbeatDailyCapBlock(agent, parseHeartbeatPolicy(agent), { - excludeRunId: run.id, - checkRunCap: true, - checkCostCap: true, - }); + const dailyCapBlock = await getHeartbeatDailyCapBlock( + agent, + parseHeartbeatPolicy(agent), + { + excludeRunId: run.id, + checkRunCap: true, + checkCostCap: true, + }, + ); if (dailyCapBlock) { await cancelQueuedRunForHeartbeatDailyCap(run, dailyCapBlock); return null; @@ -12583,17 +14478,25 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const issueId = readNonEmptyString(context.issueId); if (issueId) { - const activePauseHold = await treeControlSvc.getActivePauseHoldGate(run.companyId, issueId); - const treeHoldInteractionWake = activePauseHold && await isVerifiedIssueTreeControlInteractionWake(db, { - companyId: run.companyId, + const activePauseHold = await treeControlSvc.getActivePauseHoldGate( + run.companyId, issueId, - agentId: run.agentId, - runId: run.id, - wakeupRequestId: run.wakeupRequestId, - contextSnapshot: context, - }); + ); + const treeHoldInteractionWake = + activePauseHold && + (await isVerifiedIssueTreeControlInteractionWake(db, { + companyId: run.companyId, + issueId, + agentId: run.agentId, + runId: run.id, + wakeupRequestId: run.wakeupRequestId, + contextSnapshot: context, + })); if (activePauseHold && !treeHoldInteractionWake) { - await cancelRunInternal(run.id, "Cancelled because issue is held by an active subtree pause hold"); + await cancelRunInternal( + run.id, + "Cancelled because issue is held by an active subtree pause hold", + ); await logActivity(db, { companyId: run.companyId, actorType: "system", @@ -12609,18 +14512,32 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) holdId: activePauseHold.holdId, rootIssueId: activePauseHold.rootIssueId, source: "heartbeat.claim_queued_run", - securityPrinciples: ["Complete Mediation", "Fail Securely", "Secure Defaults"], + securityPrinciples: [ + "Complete Mediation", + "Fail Securely", + "Secure Defaults", + ], }, }); return null; } - const dependencyReadiness = await issuesSvc.listDependencyReadiness(run.companyId, [issueId]); + const dependencyReadiness = await issuesSvc.listDependencyReadiness( + run.companyId, + [issueId], + ); const readiness = dependencyReadiness.get(issueId); const unresolvedBlockerCount = readiness?.unresolvedBlockerCount ?? 0; if (unresolvedBlockerCount > 0 && !allowsIssueInteractionWake(context)) { - await cancelQueuedRunForBlockedDependencies(run, issueId, readiness?.unresolvedBlockerIssueIds ?? []); - logger.info({ runId: run.id, issueId, unresolvedBlockerCount }, "claimQueuedRun: cancelled blocked queued run"); + await cancelQueuedRunForBlockedDependencies( + run, + issueId, + readiness?.unresolvedBlockerIssueIds ?? [], + ); + logger.info( + { runId: run.id, issueId, unresolvedBlockerCount }, + "claimQueuedRun: cancelled blocked queued run", + ); return null; } @@ -12639,8 +14556,14 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const responsibleUserId = await resolveResponsibleUserIdForRun({ run, contextSnapshot: context, - issueContext: issueId ? await getIssueExecutionContext(run.companyId, issueId) : null, - routineEnvContext: { routineId: null, env: null, responsibleUserId: null }, + issueContext: issueId + ? await getIssueExecutionContext(run.companyId, issueId) + : null, + routineEnvContext: { + routineId: null, + env: null, + responsibleUserId: null, + }, }); const claimed = await db .update(heartbeatRuns) @@ -12650,7 +14573,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) startedAt: run.startedAt ?? claimedAt, updatedAt: claimedAt, }) - .where(and(eq(heartbeatRuns.id, run.id), eq(heartbeatRuns.status, "queued"))) + .where( + and(eq(heartbeatRuns.id, run.id), eq(heartbeatRuns.status, "queued")), + ) .returning() .then((rows) => rows[0] ?? null); if (!claimed) return null; @@ -12666,8 +14591,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: claimed.triggerDetail, error: claimed.error ?? null, errorCode: claimed.errorCode ?? null, - startedAt: claimed.startedAt ? new Date(claimed.startedAt).toISOString() : null, - finishedAt: claimed.finishedAt ? new Date(claimed.finishedAt).toISOString() : null, + startedAt: claimed.startedAt + ? new Date(claimed.startedAt).toISOString() + : null, + finishedAt: claimed.finishedAt + ? new Date(claimed.finishedAt).toISOString() + : null, }, }); publishRunLifecyclePluginEvent(claimed); @@ -12679,7 +14608,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const claimedContext = parseObject(claimed.contextSnapshot); const claimedIssueId = readNonEmptyString(claimedContext.issueId); const claimedWakeReason = readNonEmptyString(claimedContext.wakeReason); - if (claimedIssueId && claimedWakeReason !== "source_scoped_recovery_action") { + if ( + claimedIssueId && + claimedWakeReason !== "source_scoped_recovery_action" + ) { const claimedAgent = await getAgent(claimed.agentId); await db .update(issues) @@ -12696,7 +14628,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // Mention/context runs can touch an issue, but only the current assignee // owns the issue execution lock shown as the active run. eq(issues.assigneeAgentId, claimed.agentId), - or(isNull(issues.executionRunId), eq(issues.executionRunId, claimed.id)), + or( + isNull(issues.executionRunId), + eq(issues.executionRunId, claimed.id), + ), ), ); } @@ -12806,26 +14741,38 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const wakeCommentId = deriveCommentId(context, null); const isInteractionWake = allowsIssueInteractionWake(context); - const resumeIntent = context.resumeIntent === true || context.followUpRequested === true; + const resumeIntent = + context.resumeIntent === true || context.followUpRequested === true; const wakeReason = readNonEmptyString(context.wakeReason); - const retryReason = readNonEmptyString(context.retryReason) ?? run.scheduledRetryReason ?? null; - const interactionResolvedAt = readNonEmptyString(context.interactionResolvedAt); - const hasResolvedInteractionEvidence = interactionResolvedAt !== null && !Number.isNaN(Date.parse(interactionResolvedAt)); + const retryReason = + readNonEmptyString(context.retryReason) ?? + run.scheduledRetryReason ?? + null; + const interactionResolvedAt = readNonEmptyString( + context.interactionResolvedAt, + ); + const hasResolvedInteractionEvidence = + interactionResolvedAt !== null && + !Number.isNaN(Date.parse(interactionResolvedAt)); if ( issue.status === "in_progress" && !wakeCommentId && !hasResolvedInteractionEvidence && - (wakeReason === "issue_continuation_needed" || retryReason === "issue_continuation_needed") + (wakeReason === "issue_continuation_needed" || + retryReason === "issue_continuation_needed") ) { const queuedWake = parseObject(context.paperclipWake); const queuedContinuationSummary = - readNonEmptyString(parseObject(context.paperclipContinuationSummary).body) ?? + readNonEmptyString( + parseObject(context.paperclipContinuationSummary).body, + ) ?? readNonEmptyString(parseObject(queuedWake.continuationSummary).body); const currentContinuationSummary = queuedContinuationSummary ? null : await getIssueContinuationSummaryDocument(db, issueId); - const continuationSummaryBody = queuedContinuationSummary ?? currentContinuationSummary?.body ?? null; + const continuationSummaryBody = + queuedContinuationSummary ?? currentContinuationSummary?.body ?? null; if (continuationSummaryParksExecutor(continuationSummaryBody)) { return { stale: true, @@ -12842,11 +14789,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } - const reviewExecutionState = issue.status === "in_review" - ? parseIssueExecutionState(issue.executionState) - : null; + const reviewExecutionState = + issue.status === "in_review" + ? parseIssueExecutionState(issue.executionState) + : null; const reviewParticipant = reviewExecutionState?.currentParticipant ?? null; - const isCurrentReviewParticipant = reviewParticipant?.type === "agent" && + const isCurrentReviewParticipant = + reviewParticipant?.type === "agent" && reviewParticipant.agentId === run.agentId; if ( @@ -12879,16 +14828,26 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } - if (retryReason === MAX_TURN_CONTINUATION_RETRY_REASON && issue.status !== "in_progress") { + if ( + retryReason === MAX_TURN_CONTINUATION_RETRY_REASON && + issue.status !== "in_progress" + ) { return { stale: true, errorCode: "issue_not_in_progress", reason: `Cancelled because max-turn continuation issue is no longer in_progress (current status: ${issue.status}) before the queued run could start`, - details: { issueId, currentStatus: issue.status, requiredStatus: "in_progress" }, + details: { + issueId, + currentStatus: issue.status, + requiredStatus: "in_progress", + }, }; } - if (retryReason === MAX_TURN_CONTINUATION_RETRY_REASON && issue.executionRunId !== run.id) { + if ( + retryReason === MAX_TURN_CONTINUATION_RETRY_REASON && + issue.executionRunId !== run.id + ) { return { stale: true, errorCode: "issue_execution_lock_changed", @@ -12903,10 +14862,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } if (issue.status === "in_review") { - const currentParticipant = reviewExecutionState?.currentParticipant ?? null; + const currentParticipant = + reviewExecutionState?.currentParticipant ?? null; if (currentParticipant) { const participantMatches = - currentParticipant.type === "agent" && currentParticipant.agentId === run.agentId; + currentParticipant.type === "agent" && + currentParticipant.agentId === run.agentId; if (!participantMatches && !wakeCommentId) { return { stale: true, @@ -12979,7 +14940,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return cancelled; } - function truncateAgentErrorReason(reason: string | null | undefined): string | null { + function truncateAgentErrorReason( + reason: string | null | undefined, + ): string | null { if (!reason) return null; const trimmed = reason.trim(); if (!trimmed) return null; @@ -12999,13 +14962,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return; } - const isFirstHeartbeat = options?.wasFirstHeartbeat ?? !existing.lastHeartbeatAt; + const isFirstHeartbeat = + options?.wasFirstHeartbeat ?? !existing.lastHeartbeatAt; const runningCount = await countRunningRunsForAgent(agentId); const nextStatus = runningCount > 0 ? "running" - : outcome === "succeeded" || outcome === "interrupted" || outcome === "cancelled" || (outcome === "failed" && options?.keepIdleOnFailure) + : outcome === "succeeded" || + outcome === "interrupted" || + outcome === "cancelled" || + (outcome === "failed" && options?.keepIdleOnFailure) ? "idle" : "error"; @@ -13016,7 +14983,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // Persist a human-readable reason on the agent record when it enters // error so operators see it on the agent page without digging into run // events; clear it whenever the agent leaves error. - errorReason: nextStatus === "error" ? truncateAgentErrorReason(failureReason) : null, + errorReason: + nextStatus === "error" + ? truncateAgentErrorReason(failureReason) + : null, lastHeartbeatAt: new Date(), updatedAt: new Date(), }) @@ -13026,7 +14996,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (isFirstHeartbeat && updated) { const tc = getTelemetryClient(); - if (tc) trackAgentFirstHeartbeat(tc, { agentRole: updated.role, agentId: updated.id }); + if (tc) + trackAgentFirstHeartbeat(tc, { + agentRole: updated.role, + agentId: updated.id, + }); } if (updated) { @@ -13061,7 +15035,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) errorCode: options?.errorCode ?? null, errorMessage: options?.errorMessage ?? null, }); - return mergeHeartbeatRunStopMetadata(options?.resultJson ?? null, stopMetadata); + return mergeHeartbeatRunStopMetadata( + options?.resultJson ?? null, + stopMetadata, + ); } function countValue(value: unknown) { @@ -13070,7 +15047,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } function dateValue(value: unknown) { - if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value; + if (value instanceof Date) + return Number.isNaN(value.getTime()) ? null : value; if (typeof value === "string" || typeof value === "number") { const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? null : parsed; @@ -13094,51 +15072,59 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ): Promise { const context = parseObject(run.contextSnapshot); const contextIssueId = readNonEmptyString(context.issueId); - const continuationAttempt = asNumber(context.continuationAttempt, run.continuationAttempt ?? 0); + const continuationAttempt = asNumber( + context.continuationAttempt, + run.continuationAttempt ?? 0, + ); const issue = contextIssueId ? await db - .select({ - status: issues.status, - title: issues.title, - description: issues.description, - }) - .from(issues) - .where(and(eq(issues.companyId, run.companyId), eq(issues.id, contextIssueId))) - .then((rows) => rows[0] ?? null) + .select({ + status: issues.status, + title: issues.title, + description: issues.description, + }) + .from(issues) + .where( + and( + eq(issues.companyId, run.companyId), + eq(issues.id, contextIssueId), + ), + ) + .then((rows) => rows[0] ?? null) : null; const [commentStats] = contextIssueId ? await db - .select({ - count: sql`count(*)::int`, - latestAt: sql`max(${issueComments.createdAt})`, - }) - .from(issueComments) - .where( - and( - eq(issueComments.companyId, run.companyId), - eq(issueComments.issueId, contextIssueId), - eq(issueComments.createdByRunId, run.id), - isNull(issueComments.deletedAt), - ), - ) + .select({ + count: sql`count(*)::int`, + latestAt: sql`max(${issueComments.createdAt})`, + }) + .from(issueComments) + .where( + and( + eq(issueComments.companyId, run.companyId), + eq(issueComments.issueId, contextIssueId), + eq(issueComments.createdByRunId, run.id), + isNull(issueComments.deletedAt), + ), + ) : [{ count: 0, latestAt: null }]; const issueCommentBodies = contextIssueId ? await db - .select({ body: issueComments.body }) - .from(issueComments) - .where( - and( - eq(issueComments.companyId, run.companyId), - eq(issueComments.issueId, contextIssueId), - eq(issueComments.createdByRunId, run.id), - ), - ) - .orderBy(desc(issueComments.createdAt), desc(issueComments.id)) - .limit(5) - .then((rows) => rows.reverse().map((row) => row.body)) + .select({ body: issueComments.body }) + .from(issueComments) + .where( + and( + eq(issueComments.companyId, run.companyId), + eq(issueComments.issueId, contextIssueId), + eq(issueComments.createdByRunId, run.id), + ), + ) + .orderBy(desc(issueComments.createdAt), desc(issueComments.id)) + .limit(5) + .then((rows) => rows.reverse().map((row) => row.body)) : []; const continuationSummary = contextIssueId @@ -13147,38 +15133,41 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const [documentStats] = contextIssueId ? await db - .select({ - count: sql`count(*)::int`, - planCount: sql`count(*) filter (where ${issueDocuments.key} = 'plan')::int`, - latestAt: sql`max(${documentRevisions.createdAt})`, - }) - .from(documentRevisions) - .innerJoin(issueDocuments, eq(documentRevisions.documentId, issueDocuments.documentId)) - .where( - and( - eq(documentRevisions.companyId, run.companyId), - eq(documentRevisions.createdByRunId, run.id), - eq(issueDocuments.companyId, run.companyId), - eq(issueDocuments.issueId, contextIssueId), - sql`${issueDocuments.key} != ${ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY}`, - ), - ) + .select({ + count: sql`count(*)::int`, + planCount: sql`count(*) filter (where ${issueDocuments.key} = 'plan')::int`, + latestAt: sql`max(${documentRevisions.createdAt})`, + }) + .from(documentRevisions) + .innerJoin( + issueDocuments, + eq(documentRevisions.documentId, issueDocuments.documentId), + ) + .where( + and( + eq(documentRevisions.companyId, run.companyId), + eq(documentRevisions.createdByRunId, run.id), + eq(issueDocuments.companyId, run.companyId), + eq(issueDocuments.issueId, contextIssueId), + sql`${issueDocuments.key} != ${ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY}`, + ), + ) : [{ count: 0, planCount: 0, latestAt: null }]; const [workProductStats] = contextIssueId ? await db - .select({ - count: sql`count(*)::int`, - latestAt: sql`max(${issueWorkProducts.createdAt})`, - }) - .from(issueWorkProducts) - .where( - and( - eq(issueWorkProducts.companyId, run.companyId), - eq(issueWorkProducts.issueId, contextIssueId), - eq(issueWorkProducts.createdByRunId, run.id), - ), - ) + .select({ + count: sql`count(*)::int`, + latestAt: sql`max(${issueWorkProducts.createdAt})`, + }) + .from(issueWorkProducts) + .where( + and( + eq(issueWorkProducts.companyId, run.companyId), + eq(issueWorkProducts.issueId, contextIssueId), + eq(issueWorkProducts.createdByRunId, run.id), + ), + ) : [{ count: 0, latestAt: null }]; const [workspaceOperationStats] = await db @@ -13187,7 +15176,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) latestAt: sql`max(${workspaceOperations.startedAt})`, }) .from(workspaceOperations) - .where(and(eq(workspaceOperations.companyId, run.companyId), eq(workspaceOperations.heartbeatRunId, run.id))); + .where( + and( + eq(workspaceOperations.companyId, run.companyId), + eq(workspaceOperations.heartbeatRunId, run.id), + ), + ); const [activityStats] = await db .select({ @@ -13209,7 +15203,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) latestAt: sql`max(${heartbeatRunEvents.createdAt}) filter (where ${heartbeatRunEvents.eventType} not in ('lifecycle', 'adapter.invoke', 'error'))`, }) .from(heartbeatRunEvents) - .where(and(eq(heartbeatRunEvents.companyId, run.companyId), eq(heartbeatRunEvents.runId, run.id))); + .where( + and( + eq(heartbeatRunEvents.companyId, run.companyId), + eq(heartbeatRunEvents.runId, run.id), + ), + ); return { runStatus: run.status, @@ -13246,7 +15245,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) run: typeof heartbeatRuns.$inferSelect, resultJson?: Record | null, ) { - const classification = classifyRunLiveness(await buildRunLivenessInput(run, resultJson)); + const classification = classifyRunLiveness( + await buildRunLivenessInput(run, resultJson), + ); return db .update(heartbeatRuns) .set({ @@ -13266,9 +15267,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // `pendingCleanupAttemptsSql` clamps to the same range, so both readers yield // the same value for every input. The claim predicate compares the two values, // so this alignment lets the claim match for a malformed lease. - function readPendingCleanupRetryAttempts(metadata: Record): number { + function readPendingCleanupRetryAttempts( + metadata: Record, + ): number { const value = metadata[PENDING_CLEANUP_ATTEMPTS_METADATA_KEY]; - if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0; + if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) + return 0; return Math.min(Math.floor(value), PENDING_CLEANUP_SWEEP_ATTEMPT_CAP); } @@ -13318,7 +15322,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // flag write to a lease that left pending_cleanup or dropped below the cap // between the read and this claim. The update writes only the warned key with // `jsonb_set`, so a concurrent write to an unrelated metadata key survives. - async function claimPendingCleanupCapWarning(leaseId: string): Promise { + async function claimPendingCleanupCapWarning( + leaseId: string, + ): Promise { const now = new Date(); const claimed = await db .update(environmentLeases) @@ -13366,7 +15372,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // for that period between attempts. The sweep reads and writes the attempt // count in the lease metadata. It warns once when a lease reaches the attempt // cap and then stops the retries for that lease. - async function sweepPendingCleanupLeases(opts?: { backoffMs?: number }): Promise<{ + async function sweepPendingCleanupLeases(opts?: { + backoffMs?: number; + }): Promise<{ swept: number; destroyed: number; capped: number; @@ -13393,7 +15401,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // later tick, and the database rows below still need this sweep. The caught // exception never enters the log, because a write error can carry a // credential in its message, code, cause, or stack. - logger.warn("orphan sandbox cleanup buffer flush failed; the sweep continues"); + logger.warn( + "orphan sandbox cleanup buffer flush failed; the sweep continues", + ); } const rows = await db @@ -13466,7 +15476,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // method treats the lease as ready, so the sweep keeps its earlier // behavior. const workerReady = environmentRuntime.isPendingCleanupWorkerReady - ? await environmentRuntime.isPendingCleanupWorkerReady({ environment, lease }) + ? await environmentRuntime.isPendingCleanupWorkerReady({ + environment, + lease, + }) : true; if (!workerReady) { // Move the unavailable lease to the back of the sweep queue. Otherwise @@ -13490,7 +15503,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // Tear the sandbox down from the recorded provider config and the // cleanup-authorized secret versions. The teardown returns no value // and throws on failure, so the sweep releases the lease itself. - await environmentRuntime.retryPendingSandboxTeardown({ environment, lease }); + await environmentRuntime.retryPendingSandboxTeardown({ + environment, + lease, + }); await environmentsSvc.releaseLease(lease.id, "expired", { cleanupStatus: "success", failureReason: "pending_cleanup_retry", @@ -13542,7 +15558,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // Complete persisted native results before generic orphan recovery. The reconciler reads // the durable workspace barrier and persisted runtime mode, never the current feature flag. await reconcileNativeFinalizations(db).catch((error) => { - logger.warn({ err: error }, "failed to reconcile persisted native finalizations before orphan reaping"); + logger.warn( + { err: error }, + "failed to reconcile persisted native finalizations before orphan reaping", + ); }); // Result-less transport loss is resumed on the original run. The database @@ -13550,20 +15569,33 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // open competing recoveries; executeRun receives the exact claimed owner. const nativeResumeClaims = await dispatchNativeSessionResumptions({ db, - runnerInstanceId: runtimeEnv.PAPERCLIP_INSTANCE_ID?.trim() || "paperclip-heartbeat", + runnerInstanceId: + runtimeEnv.PAPERCLIP_INSTANCE_ID?.trim() || "paperclip-heartbeat", now, dispatch: (claim) => { - const execution = executeRun(claim.runId, { nativeLeaseOwner: claim.leaseOwner }).catch((error) => { - logger.error({ err: error, runId: claim.runId }, "persisted native session resume failed"); + const execution = executeRun(claim.runId, { + nativeLeaseOwner: claim.leaseOwner, + }).catch((error) => { + logger.error( + { err: error, runId: claim.runId }, + "persisted native session resume failed", + ); }); activeRunExecutionPromises.add(execution); - void execution.finally(() => activeRunExecutionPromises.delete(execution)); + void execution.finally(() => + activeRunExecutionPromises.delete(execution), + ); }, }).catch((error) => { - logger.warn({ err: error }, "failed to claim persisted native session resumptions"); + logger.warn( + { err: error }, + "failed to claim persisted native session resumptions", + ); return []; }); - const resumedRunIds = new Set(nativeResumeClaims.map((claim) => claim.runId)); + const resumedRunIds = new Set( + nativeResumeClaims.map((claim) => claim.runId), + ); // Find all runs stuck in "running" state (queued runs are legitimately waiting; resumeQueuedRuns handles them) const activeRuns = await db @@ -13576,22 +15608,28 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id)) .where(eq(heartbeatRuns.status, "running")); - const monitorIssueIds = [...new Set(activeRuns.flatMap(({ run }) => { - const runContext = parseObject(run.contextSnapshot); - if (readNonEmptyString(runContext.wakeReason) !== "issue_monitor_due") return []; - const issueId = readNonEmptyString(runContext.issueId); - return issueId ? [issueId] : []; - }))]; - const monitorIssues = monitorIssueIds.length > 0 - ? await db - .select({ - id: issues.id, - companyId: issues.companyId, - monitorNextCheckAt: issues.monitorNextCheckAt, - }) - .from(issues) - .where(inArray(issues.id, monitorIssueIds)) - : []; + const monitorIssueIds = [ + ...new Set( + activeRuns.flatMap(({ run }) => { + const runContext = parseObject(run.contextSnapshot); + if (readNonEmptyString(runContext.wakeReason) !== "issue_monitor_due") + return []; + const issueId = readNonEmptyString(runContext.issueId); + return issueId ? [issueId] : []; + }), + ), + ]; + const monitorIssues = + monitorIssueIds.length > 0 + ? await db + .select({ + id: issues.id, + companyId: issues.companyId, + monitorNextCheckAt: issues.monitorNextCheckAt, + }) + .from(issues) + .where(inArray(issues.id, monitorIssueIds)) + : []; const monitorNextCheckAtByIssue = new Map( monitorIssues.map((issue) => [ `${issue.companyId}:${issue.id}`, @@ -13603,7 +15641,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) for (const { run, adapterType, adapterConfig } of activeRuns) { if (resumedRunIds.has(run.id)) continue; - if (runningProcesses.has(run.id) || activeRunExecutions.has(run.id)) continue; + if (runningProcesses.has(run.id) || activeRunExecutions.has(run.id)) + continue; // Apply staleness threshold to avoid false positives if (staleThresholdMs > 0) { @@ -13612,9 +15651,14 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const nativeLocalChild = run.runtimeMode === "native"; - const tracksLocalChild = nativeLocalChild || isTrackedLocalChildProcessAdapter(adapterType); - let processPidAlive = tracksLocalChild && run.processPid && isProcessAlive(run.processPid); - let processGroupAlive = tracksLocalChild && run.processGroupId && isProcessGroupAlive(run.processGroupId); + const tracksLocalChild = + nativeLocalChild || isTrackedLocalChildProcessAdapter(adapterType); + let processPidAlive = + tracksLocalChild && run.processPid && isProcessAlive(run.processPid); + let processGroupAlive = + tracksLocalChild && + run.processGroupId && + isProcessGroupAlive(run.processGroupId); if (nativeLocalChild && (processPidAlive || processGroupAlive)) { // A native runner's reconnect URL is bound to the old run authority. // If startup resumption did not adopt it above, leaving it alive makes @@ -13640,15 +15684,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) errorCode: DETACHED_PROCESS_ERROR_CODE, }); if (detachedRun) { - await appendRunEvent(detachedRun, await nextRunEventSeq(detachedRun.id), { - eventType: "lifecycle", - stream: "system", - level: "warn", - message: detachedMessage, - payload: { - processPid: run.processPid, + await appendRunEvent( + detachedRun, + await nextRunEventSeq(detachedRun.id), + { + eventType: "lifecycle", + stream: "system", + level: "warn", + message: detachedMessage, + payload: { + processPid: run.processPid, + }, }, - }); + ); } } continue; @@ -13672,20 +15720,23 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) readNonEmptyString(runContext.wakeReason) === "issue_monitor_due" && monitorNextCheckAt !== undefined && (!monitorNextCheckAt || monitorNextCheckAt.getTime() <= now.getTime()); - const shouldRetry = (run.processLossRetryCount ?? 0) < 1 && ( - (tracksLocalChild && (!!run.processPid || !!run.processGroupId)) || - monitorDispatchLostWithoutFutureWake + const shouldRetry = + (run.processLossRetryCount ?? 0) < 1 && + ((tracksLocalChild && (!!run.processPid || !!run.processGroupId)) || + monitorDispatchLostWithoutFutureWake); + const baseMessage = buildProcessLossMessage( + run, + descendantOnlyCleanup ? { descendantOnly: true } : undefined, ); - const baseMessage = buildProcessLossMessage(run, descendantOnlyCleanup ? { descendantOnly: true } : undefined); const unmanagedBackgroundTaskEvidence = descendantOnlyCleanup ? { - kind: "orphaned_process_group_cleanup", - stopped: true, - stopReason: UNMANAGED_BACKGROUND_TASK_STOP_REASON, - reason: UNMANAGED_BACKGROUND_TASK_LIVENESS_REASON, - processPid: run.processPid ?? null, - processGroupId: run.processGroupId ?? null, - } + kind: "orphaned_process_group_cleanup", + stopped: true, + stopReason: UNMANAGED_BACKGROUND_TASK_STOP_REASON, + reason: UNMANAGED_BACKGROUND_TASK_LIVENESS_REASON, + processPid: run.processPid ?? null, + processGroupId: run.processGroupId ?? null, + } : null; let finalizedRun = await setRunStatus(run.id, "failed", { @@ -13699,15 +15750,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) { resultJson: parseObject(run.resultJson), errorCode: "process_lost", - errorMessage: shouldRetry ? `${baseMessage}; retrying once` : baseMessage, + errorMessage: shouldRetry + ? `${baseMessage}; retrying once` + : baseMessage, }, ); return unmanagedBackgroundTaskEvidence ? { - ...result, - stopReason: UNMANAGED_BACKGROUND_TASK_STOP_REASON, - unmanagedBackgroundTask: unmanagedBackgroundTaskEvidence, - } + ...result, + stopReason: UNMANAGED_BACKGROUND_TASK_STOP_REASON, + unmanagedBackgroundTask: unmanagedBackgroundTaskEvidence, + } : result; })(), }); @@ -13717,7 +15770,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); if (!finalizedRun) finalizedRun = await getRun(run.id); if (!finalizedRun) continue; - finalizedRun = await classifyAndPersistRunLiveness(finalizedRun, parseObject(finalizedRun.resultJson)) ?? finalizedRun; + finalizedRun = + (await classifyAndPersistRunLiveness( + finalizedRun, + parseObject(finalizedRun.resultJson), + )) ?? finalizedRun; await releaseEnvironmentLeasesForRun({ runId: finalizedRun.id, companyId: finalizedRun.companyId, @@ -13730,10 +15787,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const retryAgent = await getAgent(run.agentId); if (shouldRetry) { if (retryAgent) { - retriedRun = await enqueueProcessLossRetry(finalizedRun, retryAgent, now); + retriedRun = await enqueueProcessLossRetry( + finalizedRun, + retryAgent, + now, + ); } } else if (retryAgent) { - const scheduled = await scheduleInteractionContinuationInfrastructureRetryIfEligible(finalizedRun, retryAgent); + const scheduled = + await scheduleInteractionContinuationInfrastructureRetryIfEligible( + finalizedRun, + retryAgent, + ); retriedRun = scheduled?.outcome === "scheduled" ? scheduled.run : null; } @@ -13741,20 +15806,26 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) await releaseIssueExecutionAndPromote(finalizedRun); } - await appendRunEvent(finalizedRun, await nextRunEventSeq(finalizedRun.id), { - eventType: "lifecycle", - stream: "system", - level: "error", - message: shouldRetry - ? `${baseMessage}; queued retry ${retriedRun?.id ?? ""}`.trim() - : baseMessage, - payload: { - ...(run.processPid ? { processPid: run.processPid } : {}), - ...(run.processGroupId ? { processGroupId: run.processGroupId } : {}), - ...(descendantOnlyCleanup ? { descendantOnlyCleanup: true } : {}), - ...(retriedRun ? { retryRunId: retriedRun.id } : {}), + await appendRunEvent( + finalizedRun, + await nextRunEventSeq(finalizedRun.id), + { + eventType: "lifecycle", + stream: "system", + level: "error", + message: shouldRetry + ? `${baseMessage}; queued retry ${retriedRun?.id ?? ""}`.trim() + : baseMessage, + payload: { + ...(run.processPid ? { processPid: run.processPid } : {}), + ...(run.processGroupId + ? { processGroupId: run.processGroupId } + : {}), + ...(descendantOnlyCleanup ? { descendantOnlyCleanup: true } : {}), + ...(retriedRun ? { retryRunId: retriedRun.id } : {}), + }, }, - }); + ); await finalizeAgentStatus(run.agentId, "failed", baseMessage, { wasFirstHeartbeat: timerClaimWasFirstHeartbeat(run), @@ -13765,17 +15836,26 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } if (reaped.length > 0) { - logger.warn({ reapedCount: reaped.length, runIds: reaped }, "reaped orphaned heartbeat runs"); + logger.warn( + { reapedCount: reaped.length, runIds: reaped }, + "reaped orphaned heartbeat runs", + ); } // Retry stranded pending_cleanup leases on the same tick. Isolate the sweep // so its failure never hides the reaper result. The backoff equals the // reaper staleness threshold. try { - const sweep = await sweepPendingCleanupLeases({ backoffMs: staleThresholdMs }); + const sweep = await sweepPendingCleanupLeases({ + backoffMs: staleThresholdMs, + }); if (sweep.destroyed > 0 || sweep.capped > 0) { logger.warn( - { destroyed: sweep.destroyed, capped: sweep.capped, swept: sweep.swept }, + { + destroyed: sweep.destroyed, + capped: sweep.capped, + swept: sweep.swept, + }, "swept pending_cleanup environment leases", ); } @@ -13799,11 +15879,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .select({ agentId: heartbeatRuns.agentId }) .from(heartbeatRuns) .innerJoin(companies, eq(companies.id, heartbeatRuns.companyId)) - .where(and( - eq(heartbeatRuns.status, "queued"), - eq(companies.status, "active"), - cutoff ? gte(heartbeatRuns.createdAt, cutoff) : undefined, - )); + .where( + and( + eq(heartbeatRuns.status, "queued"), + eq(companies.status, "active"), + cutoff ? gte(heartbeatRuns.createdAt, cutoff) : undefined, + ), + ); const agentIds = [...new Set(queuedRuns.map((r) => r.agentId))]; for (const agentId of agentIds) { @@ -13812,7 +15894,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } async function reconcileStrandedAssignedIssues() { - return recovery.reconcileStrandedAssignedIssues({ issueCreatedAtGte: await getWorktreeExecutionCutoff() }); + return recovery.reconcileStrandedAssignedIssues({ + issueCreatedAtGte: await getWorktreeExecutionCutoff(), + }); } async function sweepStaleIssueLocks() { @@ -13821,40 +15905,73 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) function issueIdFromRunContext(contextSnapshot: unknown) { const context = parseObject(contextSnapshot); - return readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); + return ( + readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId) + ); } function issueIdFromWakePayload(payload: unknown) { const parsed = parseObject(payload); const nestedContext = parseObject(parsed[DEFERRED_WAKE_CONTEXT_KEY]); - return readNonEmptyString(parsed.issueId) ?? + return ( + readNonEmptyString(parsed.issueId) ?? readNonEmptyString(nestedContext.issueId) ?? - readNonEmptyString(nestedContext.taskId); + readNonEmptyString(nestedContext.taskId) + ); } - async function scanSilentActiveRuns(opts?: { now?: Date; companyId?: string }) { - return recovery.scanSilentActiveRuns({ ...opts, issueCreatedAtGte: await getWorktreeExecutionCutoff() }); + async function scanSilentActiveRuns(opts?: { + now?: Date; + companyId?: string; + }) { + return recovery.scanSilentActiveRuns({ + ...opts, + issueCreatedAtGte: await getWorktreeExecutionCutoff(), + }); } - async function reconcileProductivityReviews(opts?: { now?: Date; companyId?: string }) { - return productivityReviews.reconcileProductivityReviews({ ...opts, issueCreatedAtGte: await getWorktreeExecutionCutoff() }); + async function reconcileProductivityReviews(opts?: { + now?: Date; + companyId?: string; + }) { + return productivityReviews.reconcileProductivityReviews({ + ...opts, + issueCreatedAtGte: await getWorktreeExecutionCutoff(), + }); } - async function reconcileTaskWatchdogs(opts?: { companyId?: string | null; runId?: string | null }) { - return taskWatchdogs.reconcileTaskWatchdogs({ ...opts, issueCreatedAtGte: await getWorktreeExecutionCutoff() }); + async function reconcileTaskWatchdogs(opts?: { + companyId?: string | null; + runId?: string | null; + }) { + return taskWatchdogs.reconcileTaskWatchdogs({ + ...opts, + issueCreatedAtGte: await getWorktreeExecutionCutoff(), + }); } async function buildRunOutputSilence( run: Pick< typeof heartbeatRuns.$inferSelect, - "id" | "companyId" | "status" | "lastOutputAt" | "lastOutputSeq" | "lastOutputStream" | "processStartedAt" | "startedAt" | "createdAt" + | "id" + | "companyId" + | "status" + | "lastOutputAt" + | "lastOutputSeq" + | "lastOutputStream" + | "processStartedAt" + | "startedAt" + | "createdAt" >, now = new Date(), ) { return recovery.buildRunOutputSilence(run, now); } - async function buildIssueGraphLivenessAutoRecoveryPreview(opts?: { lookbackHours?: number; now?: Date }) { + async function buildIssueGraphLivenessAutoRecoveryPreview(opts?: { + lookbackHours?: number; + now?: Date; + }) { return recovery.buildIssueGraphLivenessAutoRecoveryPreview(opts); } @@ -13865,7 +15982,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) now?: Date; reescalationCooldownMs?: number; }) { - return recovery.reconcileIssueGraphLiveness({ ...opts, issueCreatedAtGte: await getWorktreeExecutionCutoff() }); + return recovery.reconcileIssueGraphLiveness({ + ...opts, + issueCreatedAtGte: await getWorktreeExecutionCutoff(), + }); } async function updateRuntimeState( @@ -13882,8 +16002,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const cachedInputTokens = usage?.cachedInputTokens ?? 0; const billingType = normalizeLedgerBillingType(result.billingType); const billedCostUsd = resolveCacheAdjustedCostUsd(result); - const additionalCostCents = normalizeBilledCostCents(billedCostUsd, billingType); - const hasTokenUsage = inputTokens > 0 || outputTokens > 0 || cachedInputTokens > 0; + const additionalCostCents = normalizeBilledCostCents( + billedCostUsd, + billingType, + ); + const hasTokenUsage = + inputTokens > 0 || outputTokens > 0 || cachedInputTokens > 0; const costStatus = resolveLedgerCostStatus({ costUsd: billedCostUsd, inputTokens, @@ -13892,7 +16016,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); const provider = result.provider ?? "unknown"; const biller = resolveLedgerBiller(result); - const ledgerScope = await resolveLedgerScopeForRun(db, agent.companyId, run); + const ledgerScope = await resolveLedgerScopeForRun( + db, + agent.companyId, + run, + ); await db .update(agentRuntimeState) @@ -13942,32 +16070,47 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const invokability = await getAgentInvokability(agent); if (!invokability.invokable) { if (shouldCancelRunsForNonInvokableAgent(invokability)) { - await cancelActiveForAgentInternal(agentId, `Cancelled because the agent is not invokable: ${invokability.reason}`); + await cancelActiveForAgentInternal( + agentId, + `Cancelled because the agent is not invokable: ${invokability.reason}`, + ); } return []; } const policy = parseHeartbeatPolicy(agent); const runningCount = await countRunningRunsForAgent(agentId); - const availableSlots = Math.max(0, policy.maxConcurrentRuns - runningCount); + const availableSlots = Math.max( + 0, + policy.maxConcurrentRuns - runningCount, + ); if (availableSlots <= 0) return []; const queuedRuns = await db .select() .from(heartbeatRuns) - .where(and( - eq(heartbeatRuns.agentId, agentId), - eq(heartbeatRuns.status, "queued"), - cutoff ? gte(heartbeatRuns.createdAt, cutoff) : undefined, - )) + .where( + and( + eq(heartbeatRuns.agentId, agentId), + eq(heartbeatRuns.status, "queued"), + cutoff ? gte(heartbeatRuns.createdAt, cutoff) : undefined, + ), + ) .orderBy(asc(heartbeatRuns.createdAt)); if (queuedRuns.length === 0) return []; - const dependencyReadiness = await listQueuedRunDependencyReadiness(agent.companyId, queuedRuns); - const queuedIssueIds = [...new Set( - queuedRuns - .map((run) => readNonEmptyString(parseObject(run.contextSnapshot).issueId)) - .filter((issueId): issueId is string => Boolean(issueId)), - )]; + const dependencyReadiness = await listQueuedRunDependencyReadiness( + agent.companyId, + queuedRuns, + ); + const queuedIssueIds = [ + ...new Set( + queuedRuns + .map((run) => + readNonEmptyString(parseObject(run.contextSnapshot).issueId), + ) + .filter((issueId): issueId is string => Boolean(issueId)), + ), + ]; const issueRows = await db .select({ id: issues.id, @@ -13977,26 +16120,54 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .from(issues) .where( queuedIssueIds.length > 0 - ? and(eq(issues.companyId, agent.companyId), inArray(issues.id, queuedIssueIds)) + ? and( + eq(issues.companyId, agent.companyId), + inArray(issues.id, queuedIssueIds), + ) : sql`false`, ); const issueById = new Map(issueRows.map((row) => [row.id, row])); const companyAgents = await listCompanyAgentOrgRows(agent.companyId); const prioritizedRuns = [...queuedRuns].sort((left, right) => { - const leftIssueId = readNonEmptyString(parseObject(left.contextSnapshot).issueId); - const rightIssueId = readNonEmptyString(parseObject(right.contextSnapshot).issueId); - const leftReadiness = leftIssueId ? dependencyReadiness.get(leftIssueId) : null; - const rightReadiness = rightIssueId ? dependencyReadiness.get(rightIssueId) : null; - const leftReady = leftIssueId ? (leftReadiness?.isDependencyReady ?? true) : true; - const rightReady = rightIssueId ? (rightReadiness?.isDependencyReady ?? true) : true; + const leftIssueId = readNonEmptyString( + parseObject(left.contextSnapshot).issueId, + ); + const rightIssueId = readNonEmptyString( + parseObject(right.contextSnapshot).issueId, + ); + const leftReadiness = leftIssueId + ? dependencyReadiness.get(leftIssueId) + : null; + const rightReadiness = rightIssueId + ? dependencyReadiness.get(rightIssueId) + : null; + const leftReady = leftIssueId + ? (leftReadiness?.isDependencyReady ?? true) + : true; + const rightReady = rightIssueId + ? (rightReadiness?.isDependencyReady ?? true) + : true; const leftIssue = leftIssueId ? issueById.get(leftIssueId) : null; const rightIssue = rightIssueId ? issueById.get(rightIssueId) : null; - const leftRank = leftIssueId ? (leftReady ? (leftIssue?.status === "in_progress" ? 0 : 1) : 3) : 2; - const rightRank = rightIssueId ? (rightReady ? (rightIssue?.status === "in_progress" ? 0 : 1) : 3) : 2; + const leftRank = leftIssueId + ? leftReady + ? leftIssue?.status === "in_progress" + ? 0 + : 1 + : 3 + : 2; + const rightRank = rightIssueId + ? rightReady + ? rightIssue?.status === "in_progress" + ? 0 + : 1 + : 3 + : 2; if (leftRank !== rightRank) return leftRank - rightRank; const leftPriorityRank = issueRunPriorityRank(leftIssue?.priority); const rightPriorityRank = issueRunPriorityRank(rightIssue?.priority); - if (leftPriorityRank !== rightPriorityRank) return leftPriorityRank - rightPriorityRank; + if (leftPriorityRank !== rightPriorityRank) + return leftPriorityRank - rightPriorityRank; return left.createdAt.getTime() - right.createdAt.getTime(); }); @@ -14010,7 +16181,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) for (const claimedRun of claimedRuns) { const execution = executeRun(claimedRun.id).catch((err) => { - logger.error({ err, runId: claimedRun.id }, "queued heartbeat execution failed"); + logger.error( + { err, runId: claimedRun.id }, + "queued heartbeat execution failed", + ); }); // Register the in-flight execution so drainActiveRunExecutions() can await // it. executeRun resolves only after its finally block finishes flushing @@ -14039,7 +16213,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // activeRunExecutionPromises, and the second await drains that run. A wakeup or // a run can add more entries as it settles, so loop until both sets are empty. async function drainActiveRunExecutions() { - while (activeWakeupPromises.size > 0 || activeRunExecutionPromises.size > 0) { + while ( + activeWakeupPromises.size > 0 || + activeRunExecutionPromises.size > 0 + ) { await Promise.allSettled([...activeWakeupPromises]); await Promise.all([...activeRunExecutionPromises]); } @@ -14056,13 +16233,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ): ReturnType { const promise = enqueueWakeup(agentId, opts); activeWakeupPromises.add(promise); - void promise.catch(() => {}).finally(() => { - activeWakeupPromises.delete(promise); - }); + void promise + .catch(() => {}) + .finally(() => { + activeWakeupPromises.delete(promise); + }); return promise; } - async function executeRun(runId: string, runOptions: { nativeLeaseOwner?: string } = {}) { + async function executeRun( + runId: string, + runOptions: { nativeLeaseOwner?: string } = {}, + ) { if ((await getSchedulingSuppression()).suppressed) return; let run = await getRun(runId); @@ -14083,946 +16265,1187 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) let nativeSessionResumeScheduled = false; try { - const agent = await getAgent(run.agentId); - if (!agent) { - await setRunStatus(runId, "failed", { - error: "Agent not found", - errorCode: "agent_not_found", - finishedAt: new Date(), - }); - await setWakeupStatus(run.wakeupRequestId, "failed", { - finishedAt: new Date(), - error: "Agent not found", - }); - const failedRun = await getRun(runId); - if (failedRun) await releaseIssueExecutionAndPromote(failedRun); - return; - } - - const runtime = await ensureRuntimeState(agent); - const context = parseObject(run.contextSnapshot); - const taskKey = deriveTaskKeyWithHeartbeatFallback(context, null); - const sessionCodec = getAdapterSessionCodec(agent.adapterType); - const issueId = readNonEmptyString(context.issueId); - let issueContext = issueId ? await getIssueExecutionContext(agent.companyId, issueId) : null; - const issueDependencyReadiness = issueId - ? await issuesSvc.listDependencyReadiness(agent.companyId, [issueId]).then((rows) => rows.get(issueId) ?? null) - : null; - if ( - issueId && - issueContext && - shouldAutoCheckoutIssueForWake({ - contextSnapshot: context, - issueStatus: issueContext.status, - issueAssigneeAgentId: issueContext.assigneeAgentId, - issueExecutionState: issueContext.executionState, - isDependencyReady: issueDependencyReadiness?.isDependencyReady ?? true, - agentId: agent.id, - }) - ) { - try { - await issuesSvc.checkout(issueId, agent.id, ["todo", "backlog", "blocked"], run.id); - context[PAPERCLIP_HARNESS_CHECKOUT_KEY] = true; - } catch (error) { - if (!isCheckoutConflictError(error)) throw error; - context[PAPERCLIP_HARNESS_CHECKOUT_KEY] = false; + const agent = await getAgent(run.agentId); + if (!agent) { + await setRunStatus(runId, "failed", { + error: "Agent not found", + errorCode: "agent_not_found", + finishedAt: new Date(), + }); + await setWakeupStatus(run.wakeupRequestId, "failed", { + finishedAt: new Date(), + error: "Agent not found", + }); + const failedRun = await getRun(runId); + if (failedRun) await releaseIssueExecutionAndPromote(failedRun); + return; } - issueContext = await getIssueExecutionContext(agent.companyId, issueId); - } - const wakeCommentId = deriveCommentId(context, null); - const wakeCommentContext = - issueContext && wakeCommentId + + const runtime = await ensureRuntimeState(agent); + const context = parseObject(run.contextSnapshot); + const providerTraceRequested = + parseObject(context.debug).providerTrace === "raw"; + let providerTraceCapture: Awaited< + ReturnType + > | null = null; + if (providerTraceRequested) { + if (context.providerTraceRequestSource === "agent_debug_setting") { + try { + await logActivity(db, { + companyId: run.companyId, + actorType: "system", + actorId: "system", + agentId: run.agentId, + runId: run.id, + action: "provider_trace.capture_requested", + entityType: "heartbeat_run", + entityId: run.id, + details: { + mode: "raw", + source: "agent_debug_setting", + retentionHours: 24, + maxBytes: 64 * 1024 * 1024, + }, + }); + } catch (error) { + logger.warn( + { error, runId: run.id }, + "provider trace capture audit could not be recorded", + ); + } + } + try { + providerTraceCapture = await traceStore.prepare({ + runId: run.id, + companyId: run.companyId, + provider: agent.adapterType, + requestedBy: + readNonEmptyString(context.providerTraceRequestedBy) ?? + "local-admin", + }); + } catch (error) { + logger.warn( + { error, runId: run.id }, + "provider trace sidecar could not be prepared", + ); + } + } + const taskKey = deriveTaskKeyWithHeartbeatFallback(context, null); + const sessionCodec = getAdapterSessionCodec(agent.adapterType); + const issueId = readNonEmptyString(context.issueId); + let issueContext = issueId + ? await getIssueExecutionContext(agent.companyId, issueId) + : null; + const issueDependencyReadiness = issueId + ? await issuesSvc + .listDependencyReadiness(agent.companyId, [issueId]) + .then((rows) => rows.get(issueId) ?? null) + : null; + if ( + issueId && + issueContext && + shouldAutoCheckoutIssueForWake({ + contextSnapshot: context, + issueStatus: issueContext.status, + issueAssigneeAgentId: issueContext.assigneeAgentId, + issueExecutionState: issueContext.executionState, + isDependencyReady: + issueDependencyReadiness?.isDependencyReady ?? true, + agentId: agent.id, + }) + ) { + try { + await issuesSvc.checkout( + issueId, + agent.id, + ["todo", "backlog", "blocked"], + run.id, + ); + context[PAPERCLIP_HARNESS_CHECKOUT_KEY] = true; + } catch (error) { + if (!isCheckoutConflictError(error)) throw error; + context[PAPERCLIP_HARNESS_CHECKOUT_KEY] = false; + } + issueContext = await getIssueExecutionContext(agent.companyId, issueId); + } + const wakeCommentId = deriveCommentId(context, null); + const wakeCommentContext = + issueContext && wakeCommentId + ? await db + .select({ + id: issueComments.id, + body: issueComments.body, + authorType: issueComments.authorType, + authorAgentId: issueComments.authorAgentId, + authorUserId: issueComments.authorUserId, + presentation: issueComments.presentation, + metadata: issueComments.metadata, + deletedAt: issueComments.deletedAt, + deletedByType: issueComments.deletedByType, + deletedByAgentId: issueComments.deletedByAgentId, + deletedByUserId: issueComments.deletedByUserId, + deletedByRunId: issueComments.deletedByRunId, + sourceTrust: issueComments.sourceTrust, + }) + .from(issueComments) + .where( + and( + eq(issueComments.id, wakeCommentId), + eq(issueComments.issueId, issueContext.id), + eq(issueComments.companyId, agent.companyId), + ), + ) + .then((rows) => { + const row = rows[0] ?? null; + return row?.deletedAt + ? { + ...row, + body: "", + presentation: null, + metadata: null, + } + : row; + }) + : null; + const issueAssigneeOverrides = + issueContext && issueContext.assigneeAgentId === agent.id + ? parseIssueAssigneeAdapterOverrides( + issueContext.assigneeAdapterOverrides, + ) + : null; + const experimentalInstanceSettings = + await instanceSettings.getExperimental(); + const isolatedWorkspacesEnabled = + experimentalInstanceSettings.enableIsolatedWorkspaces; + const parsedIssueExecutionWorkspaceSettings = + parseIssueExecutionWorkspaceSettings( + issueContext?.executionWorkspaceSettings, + ); + const issueExecutionWorkspaceSettings = isolatedWorkspacesEnabled + ? parsedIssueExecutionWorkspaceSettings + : null; + const environmentExecutionWorkspaceSettings = + selectEnvironmentExecutionWorkspaceSettings( + parsedIssueExecutionWorkspaceSettings, + isolatedWorkspacesEnabled, + ); + const contextProjectId = readNonEmptyString(context.projectId); + const executionProjectId = issueContext?.projectId ?? contextProjectId; + const projectContext = executionProjectId ? await db .select({ - id: issueComments.id, - body: issueComments.body, - authorType: issueComments.authorType, - authorAgentId: issueComments.authorAgentId, - authorUserId: issueComments.authorUserId, - presentation: issueComments.presentation, - metadata: issueComments.metadata, - deletedAt: issueComments.deletedAt, - deletedByType: issueComments.deletedByType, - deletedByAgentId: issueComments.deletedByAgentId, - deletedByUserId: issueComments.deletedByUserId, - deletedByRunId: issueComments.deletedByRunId, - sourceTrust: issueComments.sourceTrust, - }) - .from(issueComments) - .where(and( - eq(issueComments.id, wakeCommentId), - eq(issueComments.issueId, issueContext.id), - eq(issueComments.companyId, agent.companyId), - )) - .then((rows) => { - const row = rows[0] ?? null; - return row?.deletedAt - ? { - ...row, - body: "", - presentation: null, - metadata: null, - } - : row; + id: projects.id, + executionWorkspacePolicy: projects.executionWorkspacePolicy, + env: projects.env, + updatedAt: projects.updatedAt, }) + .from(projects) + .where( + and( + eq(projects.id, executionProjectId), + eq(projects.companyId, agent.companyId), + ), + ) + .then((rows) => rows[0] ?? null) : null; - const issueAssigneeOverrides = - issueContext && issueContext.assigneeAgentId === agent.id - ? parseIssueAssigneeAdapterOverrides( - issueContext.assigneeAdapterOverrides, + const acceptedPlanContinuationWake = issueContext + ? readNonEmptyString(context.workspaceRefreshReason) === + "accepted_plan_confirmation" || + (issueContext.workMode === "planning" && + readNonEmptyString(context.interactionKind) === + "request_confirmation" && + readNonEmptyString(context.interactionStatus) === "accepted") + : false; + const acceptedPlanWakeRoutingDecision = issueContext + ? await resolveAcceptedPlanWakeRoutingDecision({ + db, + companyId: agent.companyId, + agentId: agent.id, + issueId, + acceptedPlanContinuationWake, + contextSnapshot: context, + }) + : null; + if (acceptedPlanWakeRoutingDecision) { + context.forceFreshSession = true; + context.acceptedPlanWakeRouting = { + reason: "other_issue_claim_in_flight", + otherActiveClaimIssueId: + acceptedPlanWakeRoutingDecision.otherActiveClaimIssueId, + otherActiveClaimIdentifier: + acceptedPlanWakeRoutingDecision.otherActiveClaimIdentifier, + otherActiveClaimTitle: + acceptedPlanWakeRoutingDecision.otherActiveClaimTitle, + }; + if (acceptedPlanWakeRoutingDecision.suppressAcceptedContinuation) { + clearInteractionContinuationWakeContext(context); + delete context.workspaceRefreshReason; + } + } else { + delete context.acceptedPlanWakeRouting; + } + const routineEnvContext = await getRoutineEnvForExecutionIssue( + agent.companyId, + issueContext, + ); + const responsibleUserId = await resolveResponsibleUserIdForRun({ + run, + contextSnapshot: context, + issueContext, + routineEnvContext, + }); + if (responsibleUserId && run.responsibleUserId !== responsibleUserId) { + await db + .update(heartbeatRuns) + .set({ responsibleUserId, updatedAt: new Date() }) + .where(eq(heartbeatRuns.id, run.id)); + run = { ...run, responsibleUserId }; + } + if ( + responsibleUserId && + issueContext && + !issueContext.responsibleUserId + ) { + await db + .update(issues) + .set({ responsibleUserId, updatedAt: new Date() }) + .where( + and( + eq(issues.companyId, agent.companyId), + eq(issues.id, issueContext.id), + isNull(issues.responsibleUserId), + ), + ); + issueContext = { ...issueContext, responsibleUserId }; + } + const parsedProjectExecutionWorkspacePolicy = + parseProjectExecutionWorkspacePolicy( + projectContext?.executionWorkspacePolicy, + ); + const projectExecutionWorkspacePolicy = + gateProjectExecutionWorkspacePolicy( + parsedProjectExecutionWorkspacePolicy, + isolatedWorkspacesEnabled, + ); + const trustPreset = resolveCoreTrustPreset({ + companyId: agent.companyId, + agent: { + companyId: agent.companyId, + permissions: agent.permissions, + }, + project: projectContext + ? { + companyId: agent.companyId, + executionWorkspacePolicy: projectExecutionWorkspacePolicy, + } + : null, + issue: issueContext + ? { + companyId: agent.companyId, + executionPolicy: issueContext.executionPolicy, + } + : null, + }); + const config = parseObject(agent.adapterConfig); + const taskSession = taskKey + ? await getTaskSession( + agent.companyId, + agent.id, + agent.adapterType, + taskKey, ) : null; - const experimentalInstanceSettings = await instanceSettings.getExperimental(); - const isolatedWorkspacesEnabled = experimentalInstanceSettings.enableIsolatedWorkspaces; - const parsedIssueExecutionWorkspaceSettings = parseIssueExecutionWorkspaceSettings( - issueContext?.executionWorkspaceSettings, - ); - const issueExecutionWorkspaceSettings = isolatedWorkspacesEnabled - ? parsedIssueExecutionWorkspaceSettings - : null; - const environmentExecutionWorkspaceSettings = selectEnvironmentExecutionWorkspaceSettings( - parsedIssueExecutionWorkspaceSettings, - isolatedWorkspacesEnabled, - ); - const contextProjectId = readNonEmptyString(context.projectId); - const executionProjectId = issueContext?.projectId ?? contextProjectId; - const projectContext = executionProjectId - ? await db - .select({ - id: projects.id, - executionWorkspacePolicy: projects.executionWorkspacePolicy, - env: projects.env, - updatedAt: projects.updatedAt, - }) - .from(projects) - .where(and(eq(projects.id, executionProjectId), eq(projects.companyId, agent.companyId))) - .then((rows) => rows[0] ?? null) - : null; - const acceptedPlanContinuationWake = issueContext - ? readNonEmptyString(context.workspaceRefreshReason) === "accepted_plan_confirmation" - || ( - issueContext.workMode === "planning" - && readNonEmptyString(context.interactionKind) === "request_confirmation" - && readNonEmptyString(context.interactionStatus) === "accepted" - ) - : false; - const acceptedPlanWakeRoutingDecision = issueContext - ? await resolveAcceptedPlanWakeRoutingDecision({ - db, - companyId: agent.companyId, - agentId: agent.id, - issueId, - acceptedPlanContinuationWake, - contextSnapshot: context, - }) - : null; - if (acceptedPlanWakeRoutingDecision) { - context.forceFreshSession = true; - context.acceptedPlanWakeRouting = { - reason: "other_issue_claim_in_flight", - otherActiveClaimIssueId: acceptedPlanWakeRoutingDecision.otherActiveClaimIssueId, - otherActiveClaimIdentifier: acceptedPlanWakeRoutingDecision.otherActiveClaimIdentifier, - otherActiveClaimTitle: acceptedPlanWakeRoutingDecision.otherActiveClaimTitle, - }; - if (acceptedPlanWakeRoutingDecision.suppressAcceptedContinuation) { - clearInteractionContinuationWakeContext(context); - delete context.workspaceRefreshReason; - } - } else { - delete context.acceptedPlanWakeRouting; - } - const routineEnvContext = await getRoutineEnvForExecutionIssue(agent.companyId, issueContext); - const responsibleUserId = await resolveResponsibleUserIdForRun({ - run, - contextSnapshot: context, - issueContext, - routineEnvContext, - }); - if (responsibleUserId && run.responsibleUserId !== responsibleUserId) { - await db - .update(heartbeatRuns) - .set({ responsibleUserId, updatedAt: new Date() }) - .where(eq(heartbeatRuns.id, run.id)); - run = { ...run, responsibleUserId }; - } - if (responsibleUserId && issueContext && !issueContext.responsibleUserId) { - await db - .update(issues) - .set({ responsibleUserId, updatedAt: new Date() }) - .where(and(eq(issues.companyId, agent.companyId), eq(issues.id, issueContext.id), isNull(issues.responsibleUserId))); - issueContext = { ...issueContext, responsibleUserId }; - } - const parsedProjectExecutionWorkspacePolicy = parseProjectExecutionWorkspacePolicy( - projectContext?.executionWorkspacePolicy, - ); - const projectExecutionWorkspacePolicy = gateProjectExecutionWorkspacePolicy( - parsedProjectExecutionWorkspacePolicy, - isolatedWorkspacesEnabled, - ); - const trustPreset = resolveCoreTrustPreset({ - companyId: agent.companyId, - agent: { - companyId: agent.companyId, - permissions: agent.permissions, - }, - project: projectContext + const taskSessionDecodedParams = normalizeSessionParams( + sessionCodec.deserialize(taskSession?.sessionParamsJson ?? null), + ); + const explicitResumeSessionParams = normalizeResumeParamsForAdapter( + agent.adapterType, + sessionCodec.deserialize(parseObject(context.resumeSessionParams)), + ); + const explicitResumeSessionDisplayId = truncateDisplayId( + readNonEmptyString(context.resumeSessionDisplayId) ?? + (sessionCodec.getDisplayId + ? sessionCodec.getDisplayId(explicitResumeSessionParams) + : null) ?? + readNonEmptyString(explicitResumeSessionParams?.sessionId), + ); + const resolvedExecutionWorkspaceMode = resolveExecutionWorkspaceMode({ + projectPolicy: projectExecutionWorkspacePolicy, + issueSettings: issueExecutionWorkspaceSettings, + legacyUseProjectWorkspace: + issueAssigneeOverrides?.useProjectWorkspace ?? null, + }); + const requestedExecutionWorkspaceMode = + trustPreset.kind === "low_trust_review" && + resolvedExecutionWorkspaceMode === "shared_workspace" + ? "isolated_workspace" + : resolvedExecutionWorkspaceMode; + const issueRef = issueContext ? { - companyId: agent.companyId, - executionWorkspacePolicy: projectExecutionWorkspacePolicy, + id: issueContext.id, + identifier: issueContext.identifier, + title: issueContext.title, + status: issueContext.status, + priority: issueContext.priority, + workMode: issueContext.workMode, + description: issueContext.description, + projectId: issueContext.projectId, + projectWorkspaceId: issueContext.projectWorkspaceId, + executionWorkspaceId: issueContext.executionWorkspaceId, + executionWorkspacePreference: + issueContext.executionWorkspacePreference, } - : null, - issue: issueContext - ? { - companyId: agent.companyId, - executionPolicy: issueContext.executionPolicy, - } - : null, - }); - const config = parseObject(agent.adapterConfig); - const taskSession = taskKey - ? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey) - : null; - const taskSessionDecodedParams = normalizeSessionParams( - sessionCodec.deserialize(taskSession?.sessionParamsJson ?? null), - ); - const explicitResumeSessionParams = normalizeResumeParamsForAdapter( - agent.adapterType, - sessionCodec.deserialize(parseObject(context.resumeSessionParams)), - ); - const explicitResumeSessionDisplayId = truncateDisplayId( - readNonEmptyString(context.resumeSessionDisplayId) ?? - (sessionCodec.getDisplayId ? sessionCodec.getDisplayId(explicitResumeSessionParams) : null) ?? - readNonEmptyString(explicitResumeSessionParams?.sessionId), - ); - const resolvedExecutionWorkspaceMode = resolveExecutionWorkspaceMode({ - projectPolicy: projectExecutionWorkspacePolicy, - issueSettings: issueExecutionWorkspaceSettings, - legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null, - }); - const requestedExecutionWorkspaceMode = - trustPreset.kind === "low_trust_review" && resolvedExecutionWorkspaceMode === "shared_workspace" - ? "isolated_workspace" - : resolvedExecutionWorkspaceMode; - const issueRef = issueContext - ? { - id: issueContext.id, - identifier: issueContext.identifier, - title: issueContext.title, - status: issueContext.status, - priority: issueContext.priority, - workMode: issueContext.workMode, - description: issueContext.description, - projectId: issueContext.projectId, - projectWorkspaceId: issueContext.projectWorkspaceId, - executionWorkspaceId: issueContext.executionWorkspaceId, - executionWorkspacePreference: issueContext.executionWorkspacePreference, - } - : null; - const continuationSummary = issueRef - ? await getIssueContinuationSummaryDocument(db, issueRef.id) - : null; - const exposeLowTrustRaw = trustPreset.kind === "low_trust_review"; - const safeContinuationSummary = - continuationSummary && !exposeLowTrustRaw - ? redactQuarantinedBodyForHigherTrust(continuationSummary) - : continuationSummary; - const safeWakeCommentContext = - wakeCommentContext && !exposeLowTrustRaw - ? sanitizeQuarantinedCommentForHigherTrust(wakeCommentContext) - : wakeCommentContext; - const issueAncestors = issueRef - ? await issuesSvc.getAncestors(issueRef.id) - : []; - if (continuationSummary) { - context.paperclipContinuationSummary = { - key: safeContinuationSummary!.key, - title: safeContinuationSummary!.title, - body: safeContinuationSummary!.body, - sourceTrust: safeContinuationSummary!.sourceTrust ?? null, - updatedAt: safeContinuationSummary!.updatedAt.toISOString(), - }; - } else { - delete context.paperclipContinuationSummary; - } - const pinnedSkillTestContext = - issueRef?.workMode === "skill_test" - ? await getPinnedSkillTestContext(agent.companyId, issueRef.id) : null; - if (pinnedSkillTestContext) { - context.paperclipSkillTest = { - ...pinnedSkillTestContext, - directive: "Use this pinned file inventory as the exact skill revision under test, regardless of synced runtime skills.", + const continuationSummary = issueRef + ? await getIssueContinuationSummaryDocument(db, issueRef.id) + : null; + const exposeLowTrustRaw = trustPreset.kind === "low_trust_review"; + const safeContinuationSummary = + continuationSummary && !exposeLowTrustRaw + ? redactQuarantinedBodyForHigherTrust(continuationSummary) + : continuationSummary; + const safeWakeCommentContext = + wakeCommentContext && !exposeLowTrustRaw + ? sanitizeQuarantinedCommentForHigherTrust(wakeCommentContext) + : wakeCommentContext; + const issueAncestors = issueRef + ? await issuesSvc.getAncestors(issueRef.id) + : []; + if (continuationSummary) { + context.paperclipContinuationSummary = { + key: safeContinuationSummary!.key, + title: safeContinuationSummary!.title, + body: safeContinuationSummary!.body, + sourceTrust: safeContinuationSummary!.sourceTrust ?? null, + updatedAt: safeContinuationSummary!.updatedAt.toISOString(), + }; + } else { + delete context.paperclipContinuationSummary; + } + const pinnedSkillTestContext = + issueRef?.workMode === "skill_test" + ? await getPinnedSkillTestContext(agent.companyId, issueRef.id) + : null; + if (pinnedSkillTestContext) { + context.paperclipSkillTest = { + ...pinnedSkillTestContext, + directive: + "Use this pinned file inventory as the exact skill revision under test, regardless of synced runtime skills.", + }; + } else { + delete context.paperclipSkillTest; + } + const paperclipWakePayload = await buildPaperclipWakePayload({ + db, + companyId: agent.companyId, + contextSnapshot: context, + continuationSummary, + issueSummary: issueRef + ? { + id: issueRef.id, + identifier: issueRef.identifier, + title: issueRef.title, + description: issueContext?.description ?? null, + status: issueRef.status, + priority: issueRef.priority, + workMode: issueRef.workMode, + projectId: issueRef.projectId, + executionPolicy: issueContext?.executionPolicy ?? null, + } + : null, + exposeLowTrustRaw, + simplifiedEnglishInteractions: + experimentalInstanceSettings.enableSimplifiedEnglishInteractions === + true, + }); + if (paperclipWakePayload) { + context[PAPERCLIP_WAKE_PAYLOAD_KEY] = paperclipWakePayload; + } else { + delete context[PAPERCLIP_WAKE_PAYLOAD_KEY]; + } + const taskMarkdownInput = { + issue: issueRef + ? { + id: issueRef.id, + identifier: issueRef.identifier, + title: issueRef.title, + workMode: issueRef.workMode, + description: issueRef.description, + } + : null, + ancestors: issueAncestors, + wakeComment: safeWakeCommentContext, + interaction: { + kind: readNonEmptyString(context.interactionKind), + status: readNonEmptyString(context.interactionStatus), + }, + acceptedPlanContinuation: + readNonEmptyString(context.workspaceRefreshReason) === + "accepted_plan_confirmation" && + Object.keys(parseObject(context.acceptedPlanWakeRouting)).length === + 0, }; - } else { - delete context.paperclipSkillTest; - } - const paperclipWakePayload = await buildPaperclipWakePayload({ - db, - companyId: agent.companyId, - contextSnapshot: context, - continuationSummary, - issueSummary: issueRef - ? { - id: issueRef.id, - identifier: issueRef.identifier, - title: issueRef.title, - description: issueContext?.description ?? null, - status: issueRef.status, - priority: issueRef.priority, - workMode: issueRef.workMode, - projectId: issueRef.projectId, - executionPolicy: issueContext?.executionPolicy ?? null, - } - : null, - exposeLowTrustRaw, - simplifiedEnglishInteractions: experimentalInstanceSettings.enableSimplifiedEnglishInteractions === true, - }); - if (paperclipWakePayload) { - context[PAPERCLIP_WAKE_PAYLOAD_KEY] = paperclipWakePayload; - } else { - delete context[PAPERCLIP_WAKE_PAYLOAD_KEY]; - } - const taskMarkdownInput = { - issue: issueRef - ? { - id: issueRef.id, - identifier: issueRef.identifier, - title: issueRef.title, - workMode: issueRef.workMode, - description: issueRef.description, - } - : null, - ancestors: issueAncestors, - wakeComment: safeWakeCommentContext, - interaction: { - kind: readNonEmptyString(context.interactionKind), - status: readNonEmptyString(context.interactionStatus), - }, - acceptedPlanContinuation: - readNonEmptyString(context.workspaceRefreshReason) === "accepted_plan_confirmation" - && Object.keys(parseObject(context.acceptedPlanWakeRouting)).length === 0, - }; - const taskMarkdown = buildPaperclipTaskMarkdown(taskMarkdownInput); - const taskMarkdownCompact = buildPaperclipTaskMarkdown({ ...taskMarkdownInput, includeDescription: false }); - if (issueRef) { - context.paperclipIssue = { - id: issueRef.id, - identifier: issueRef.identifier, - title: issueRef.title, - description: issueRef.description, - workMode: issueRef.workMode, - }; - } else { - delete context.paperclipIssue; - } - if (wakeCommentContext) { - context.paperclipWakeComment = safeWakeCommentContext; - } else { - delete context.paperclipWakeComment; - } - if (taskMarkdown) { - context.paperclipTaskMarkdown = taskMarkdown; - } else { - delete context.paperclipTaskMarkdown; - } - if (taskMarkdownCompact && taskMarkdownCompact !== taskMarkdown) { - context.paperclipTaskMarkdownCompact = taskMarkdownCompact; - } else { - delete context.paperclipTaskMarkdownCompact; - } - if (issueRef) { - const redactedWakeContext = await createRunSecretRedactionRegistry(db).redactForIssue( - agent.companyId, - issueRef.id, - { + const taskMarkdown = buildPaperclipTaskMarkdown(taskMarkdownInput); + const taskMarkdownCompact = buildPaperclipTaskMarkdown({ + ...taskMarkdownInput, + includeDescription: false, + }); + if (issueRef) { + context.paperclipIssue = { + id: issueRef.id, + identifier: issueRef.identifier, + title: issueRef.title, + description: issueRef.description, + workMode: issueRef.workMode, + }; + } else { + delete context.paperclipIssue; + } + if (wakeCommentContext) { + context.paperclipWakeComment = safeWakeCommentContext; + } else { + delete context.paperclipWakeComment; + } + if (taskMarkdown) { + context.paperclipTaskMarkdown = taskMarkdown; + } else { + delete context.paperclipTaskMarkdown; + } + if (taskMarkdownCompact && taskMarkdownCompact !== taskMarkdown) { + context.paperclipTaskMarkdownCompact = taskMarkdownCompact; + } else { + delete context.paperclipTaskMarkdownCompact; + } + if (issueRef) { + const redactedWakeContext = await createRunSecretRedactionRegistry( + db, + ).redactForIssue(agent.companyId, issueRef.id, { paperclipIssue: context.paperclipIssue, paperclipWakeComment: context.paperclipWakeComment, paperclipTaskMarkdown: context.paperclipTaskMarkdown, paperclipTaskMarkdownCompact: context.paperclipTaskMarkdownCompact, - }, - ); - context.paperclipIssue = redactedWakeContext.paperclipIssue; - if (redactedWakeContext.paperclipWakeComment) { - context.paperclipWakeComment = redactedWakeContext.paperclipWakeComment; - } - if (redactedWakeContext.paperclipTaskMarkdown) { - context.paperclipTaskMarkdown = redactedWakeContext.paperclipTaskMarkdown; - } - if (redactedWakeContext.paperclipTaskMarkdownCompact) { - context.paperclipTaskMarkdownCompact = redactedWakeContext.paperclipTaskMarkdownCompact; - } - } - // A native run's execution input is immutable once persisted. Recovery must therefore - // restore the workspace bound to that input rather than consulting the issue's current - // workspace pointer: a newer run may already have moved or cleared the issue binding while - // this older provider session is still recoverable. - const persistedRunnerProfile = parseObject(run.runnerProfileJson); - const persistedNativeExecutionInput = - run.runtimeMode === "native" && persistedRunnerProfile.nativeExecutionInput !== undefined - ? parseNativeExecutionInput(persistedRunnerProfile.nativeExecutionInput) - : null; - const nativeRecoveryExecutionWorkspaceId = - persistedNativeExecutionInput?.binding.executionWorkspaceId ?? null; - const requestedExecutionWorkspaceId = nativeRecoveryExecutionWorkspaceId - ?? readNonEmptyString(issueRef?.executionWorkspaceId); - const existingExecutionWorkspace = - requestedExecutionWorkspaceId ? await executionWorkspacesSvc.getById(requestedExecutionWorkspaceId) : null; - const workspaceReuseRequest = resolveExecutionWorkspaceReuseRequestForIssue({ - issueExecutionWorkspaceId: requestedExecutionWorkspaceId, - issueExecutionWorkspacePreference: nativeRecoveryExecutionWorkspaceId - ? "reuse_existing" - : issueRef?.executionWorkspacePreference ?? null, - existingExecutionWorkspaceStatus: existingExecutionWorkspace?.status ?? null, - }); - const requestedShouldReuseExisting = workspaceReuseRequest.requestedShouldReuseExisting; - const reusableExistingExecutionWorkspace = workspaceReuseRequest.existingExecutionWorkspaceAvailable - ? existingExecutionWorkspace - : null; - const requestedReusableExecutionWorkspaceConfig = reusableExistingExecutionWorkspace?.config ?? null; - const localEnvironment = await environmentsSvc.ensureLocalEnvironment(agent.companyId); - const resolvedInstanceSettings = await instanceSettings.get(); - // Managed-sandbox-only policy: a run that would land on the local - // environment is redirected onto the platform-managed sandbox row, and - // with no active managed row the resolution fails closed - // (ManagedSandboxUnavailableError) — never local. Mirrors the forced - // kubernetes execution mode below, which takes precedence when both - // regimes are active. - const managedSandboxOnly = - (await instanceSettings.getExperimental()).enableManagedSandboxOnly === true; - const managedSandboxEnvironment = managedSandboxOnly - ? await environmentsSvc.findManagedSandboxEnvironment(agent.companyId) - : null; - const environmentResolution = resolveExecutionWorkspaceEnvironmentId({ - agentDefaultEnvironmentId: agent.defaultEnvironmentId, - instanceDefaultEnvironmentId: resolvedInstanceSettings.defaultEnvironmentId ?? null, - localDefaultEnvironmentId: localEnvironment.id, - managedSandboxOnly, - managedSandboxEnvironmentId: managedSandboxEnvironment?.id ?? null, - }); - const effectiveExecutionWorkspaceMode: ReturnType = - requestedExecutionWorkspaceMode; - const executionPolicy = { - executionMode: resolvedInstanceSettings.general.executionMode, - // Backstop behind the resolver's local→managed redirect: the run-time - // allowlist below fails any run that still resolved to a `local` - // environment under managed-sandbox-only, so no selection path or - // tenant-set env var can land untrusted execution on the tenant - // container. - managedSandboxOnly, - }; - const executionForcedToKubernetes = isExecutionForcedToKubernetes(executionPolicy); - let selectedEnvironmentId = environmentResolution.environmentId; - if (executionForcedToKubernetes) { - let kubernetesEnvironment = await environmentsSvc.findKubernetesEnvironment(agent.companyId); - if (!kubernetesEnvironment) { - // Lazy recovery for companies created after the startup bootstrap ran - // (the boot hook only provisions environments for companies that exist - // at boot). Re-derive the managed-env config from the bootstrap env. - // If the process env no longer forces Kubernetes (rollback / config - // drift relative to the persisted executionMode setting), skip the - // provisioning gracefully: the guard below still refuses local - // fallback with the explicit error, instead of crashing here on - // undefined config. - let bootstrap: ReturnType = null; - let bootstrapSkipReason: string | null = null; - try { - bootstrap = parseExecutionPolicyBootstrapEnv(process.env); - if (!bootstrap) { - bootstrapSkipReason = - 'PAPERCLIP_EXECUTION_MODE bootstrap env is not kubernetes-forced (absent or "any")'; - } - } catch (err) { - bootstrapSkipReason = `PAPERCLIP_EXECUTION_MODE bootstrap env failed to parse: ${ - err instanceof Error ? err.message : String(err) - }`; + }); + context.paperclipIssue = redactedWakeContext.paperclipIssue; + if (redactedWakeContext.paperclipWakeComment) { + context.paperclipWakeComment = + redactedWakeContext.paperclipWakeComment; } - if (bootstrap) { - await environmentsSvc.ensureKubernetesEnvironment( - agent.companyId, - bootstrap.kubernetesConfig, + if (redactedWakeContext.paperclipTaskMarkdown) { + context.paperclipTaskMarkdown = + redactedWakeContext.paperclipTaskMarkdown; + } + if (redactedWakeContext.paperclipTaskMarkdownCompact) { + context.paperclipTaskMarkdownCompact = + redactedWakeContext.paperclipTaskMarkdownCompact; + } + } + // A native run's execution input is immutable once persisted. Recovery must therefore + // restore the workspace bound to that input rather than consulting the issue's current + // workspace pointer: a newer run may already have moved or cleared the issue binding while + // this older provider session is still recoverable. + const persistedRunnerProfile = parseObject(run.runnerProfileJson); + const persistedNativeExecutionInput = + run.runtimeMode === "native" && + persistedRunnerProfile.nativeExecutionInput !== undefined + ? parseNativeExecutionInput( + persistedRunnerProfile.nativeExecutionInput, + ) + : null; + const nativeRecoveryExecutionWorkspaceId = + persistedNativeExecutionInput?.binding.executionWorkspaceId ?? null; + const requestedExecutionWorkspaceId = + nativeRecoveryExecutionWorkspaceId ?? + readNonEmptyString(issueRef?.executionWorkspaceId); + const existingExecutionWorkspace = requestedExecutionWorkspaceId + ? await executionWorkspacesSvc.getById(requestedExecutionWorkspaceId) + : null; + const workspaceReuseRequest = + resolveExecutionWorkspaceReuseRequestForIssue({ + issueExecutionWorkspaceId: requestedExecutionWorkspaceId, + issueExecutionWorkspacePreference: nativeRecoveryExecutionWorkspaceId + ? "reuse_existing" + : (issueRef?.executionWorkspacePreference ?? null), + existingExecutionWorkspaceStatus: + existingExecutionWorkspace?.status ?? null, + }); + const requestedShouldReuseExisting = + workspaceReuseRequest.requestedShouldReuseExisting; + const reusableExistingExecutionWorkspace = + workspaceReuseRequest.existingExecutionWorkspaceAvailable + ? existingExecutionWorkspace + : null; + const requestedReusableExecutionWorkspaceConfig = + reusableExistingExecutionWorkspace?.config ?? null; + const localEnvironment = await environmentsSvc.ensureLocalEnvironment( + agent.companyId, + ); + const resolvedInstanceSettings = await instanceSettings.get(); + // Managed-sandbox-only policy: a run that would land on the local + // environment is redirected onto the platform-managed sandbox row, and + // with no active managed row the resolution fails closed + // (ManagedSandboxUnavailableError) — never local. Mirrors the forced + // kubernetes execution mode below, which takes precedence when both + // regimes are active. + const managedSandboxOnly = + (await instanceSettings.getExperimental()).enableManagedSandboxOnly === + true; + const managedSandboxEnvironment = managedSandboxOnly + ? await environmentsSvc.findManagedSandboxEnvironment(agent.companyId) + : null; + const environmentResolution = resolveExecutionWorkspaceEnvironmentId({ + agentDefaultEnvironmentId: agent.defaultEnvironmentId, + instanceDefaultEnvironmentId: + resolvedInstanceSettings.defaultEnvironmentId ?? null, + localDefaultEnvironmentId: localEnvironment.id, + managedSandboxOnly, + managedSandboxEnvironmentId: managedSandboxEnvironment?.id ?? null, + }); + const effectiveExecutionWorkspaceMode: ReturnType< + typeof resolveExecutionWorkspaceMode + > = requestedExecutionWorkspaceMode; + const executionPolicy = { + executionMode: resolvedInstanceSettings.general.executionMode, + // Backstop behind the resolver's local→managed redirect: the run-time + // allowlist below fails any run that still resolved to a `local` + // environment under managed-sandbox-only, so no selection path or + // tenant-set env var can land untrusted execution on the tenant + // container. + managedSandboxOnly, + }; + const executionForcedToKubernetes = + isExecutionForcedToKubernetes(executionPolicy); + let selectedEnvironmentId = environmentResolution.environmentId; + if (executionForcedToKubernetes) { + let kubernetesEnvironment = + await environmentsSvc.findKubernetesEnvironment(agent.companyId); + if (!kubernetesEnvironment) { + // Lazy recovery for companies created after the startup bootstrap ran + // (the boot hook only provisions environments for companies that exist + // at boot). Re-derive the managed-env config from the bootstrap env. + // If the process env no longer forces Kubernetes (rollback / config + // drift relative to the persisted executionMode setting), skip the + // provisioning gracefully: the guard below still refuses local + // fallback with the explicit error, instead of crashing here on + // undefined config. + let bootstrap: ReturnType = + null; + let bootstrapSkipReason: string | null = null; + try { + bootstrap = parseExecutionPolicyBootstrapEnv(process.env); + if (!bootstrap) { + bootstrapSkipReason = + 'PAPERCLIP_EXECUTION_MODE bootstrap env is not kubernetes-forced (absent or "any")'; + } + } catch (err) { + bootstrapSkipReason = `PAPERCLIP_EXECUTION_MODE bootstrap env failed to parse: ${ + err instanceof Error ? err.message : String(err) + }`; + } + if (bootstrap) { + await environmentsSvc.ensureKubernetesEnvironment( + agent.companyId, + bootstrap.kubernetesConfig, + ); + kubernetesEnvironment = + await environmentsSvc.findKubernetesEnvironment(agent.companyId); + } else { + logger.warn( + { + runId: run.id, + agentId: agent.id, + companyId: agent.companyId, + reason: bootstrapSkipReason, + }, + "executionMode=kubernetes is persisted but the bootstrap env cannot provision a managed Kubernetes environment; skipping lazy provisioning for this company (the run will fail with the explicit no-managed-environment error)", + ); + } + } + if (!kubernetesEnvironment) { + throw new Error( + "Instance execution policy requires the Kubernetes sandbox provider " + + "(executionMode=kubernetes) but no managed Kubernetes environment is " + + "configured for this company. Configure one (PAPERCLIP_K8S_* env on the " + + "cloud instance) before running agents; refusing to fall back to local execution.", ); - kubernetesEnvironment = await environmentsSvc.findKubernetesEnvironment(agent.companyId); - } else { - logger.warn( + } + if (kubernetesEnvironment.id !== selectedEnvironmentId) { + logger.info( { runId: run.id, + issueId, agentId: agent.id, - companyId: agent.companyId, - reason: bootstrapSkipReason, + resolvedEnvironmentId: selectedEnvironmentId, + forcedKubernetesEnvironmentId: kubernetesEnvironment.id, }, - "executionMode=kubernetes is persisted but the bootstrap env cannot provision a managed Kubernetes environment; skipping lazy provisioning for this company (the run will fail with the explicit no-managed-environment error)", + "Forcing run onto the managed Kubernetes environment (executionMode=kubernetes)", ); } + selectedEnvironmentId = kubernetesEnvironment.id; } - if (!kubernetesEnvironment) { - throw new Error( - "Instance execution policy requires the Kubernetes sandbox provider " + - "(executionMode=kubernetes) but no managed Kubernetes environment is " + - "configured for this company. Configure one (PAPERCLIP_K8S_* env on the " + - "cloud instance) before running agents; refusing to fall back to local execution.", - ); - } - if (kubernetesEnvironment.id !== selectedEnvironmentId) { - logger.info( - { - runId: run.id, - issueId, - agentId: agent.id, - resolvedEnvironmentId: selectedEnvironmentId, - forcedKubernetesEnvironmentId: kubernetesEnvironment.id, - }, - "Forcing run onto the managed Kubernetes environment (executionMode=kubernetes)", - ); - } - selectedEnvironmentId = kubernetesEnvironment.id; - } - const selectedEnvironmentForConfig = selectedEnvironmentId === localEnvironment.id - ? localEnvironment - : selectedEnvironmentId - ? await environmentsSvc.getById(selectedEnvironmentId) - : null; - const sharedWorkspaceConcurrency = resolveSharedWorkspaceConcurrency({ - projectPolicy: projectExecutionWorkspacePolicy, - issueSettings: issueExecutionWorkspaceSettings, - }); - // A live holder is always consulted for shared workspaces. Depending on policy and the final - // execution target it either remains the existing deferral gate or becomes dispatch context. - // Holder staleness and the workspace_busy retry ladder are intentionally unchanged for every - // path that serializes. - if (issueRef?.projectWorkspaceId && effectiveExecutionWorkspaceMode === "shared_workspace") { - const workspaceHolder = await findSharedWorkspaceHolder({ - companyId: agent.companyId, - projectWorkspaceId: issueRef.projectWorkspaceId, - excludeIssueId: issueRef.id, - excludeRunId: run.id, - honorIsolatedWorkspaceModes: isolatedWorkspacesEnabled, + const selectedEnvironmentForConfig = + selectedEnvironmentId === localEnvironment.id + ? localEnvironment + : selectedEnvironmentId + ? await environmentsSvc.getById(selectedEnvironmentId) + : null; + const sharedWorkspaceConcurrency = resolveSharedWorkspaceConcurrency({ + projectPolicy: projectExecutionWorkspacePolicy, + issueSettings: issueExecutionWorkspaceSettings, }); - if (workspaceHolder) { - const environmentDriver = selectedEnvironmentForConfig?.driver ?? null; - const shouldSerialize = sharedWorkspaceConcurrency === "serialize" - || ( - sharedWorkspaceConcurrency === "auto" - && ( - executionForcedToKubernetes - || (environmentDriver !== "local" && environmentDriver !== "ssh") - ) - ); - if (shouldSerialize) { - throw new WorkspaceBusyDeferral({ - holder: workspaceHolder, - projectWorkspaceId: issueRef.projectWorkspaceId, - deferralAttempt: - run.scheduledRetryReason === WORKSPACE_BUSY_RETRY_REASON - ? (run.scheduledRetryAttempt ?? 0) - : 0, - wasIssueAssignee: issueContext?.assigneeAgentId === agent.id, - }); - } + // A live holder is always consulted for shared workspaces. Depending on policy and the final + // execution target it either remains the existing deferral gate or becomes dispatch context. + // Holder staleness and the workspace_busy retry ladder are intentionally unchanged for every + // path that serializes. + if ( + issueRef?.projectWorkspaceId && + effectiveExecutionWorkspaceMode === "shared_workspace" + ) { + const workspaceHolder = await findSharedWorkspaceHolder({ + companyId: agent.companyId, + projectWorkspaceId: issueRef.projectWorkspaceId, + excludeIssueId: issueRef.id, + excludeRunId: run.id, + honorIsolatedWorkspaceModes: isolatedWorkspacesEnabled, + }); + if (workspaceHolder) { + const environmentDriver = + selectedEnvironmentForConfig?.driver ?? null; + const shouldSerialize = + sharedWorkspaceConcurrency === "serialize" || + (sharedWorkspaceConcurrency === "auto" && + (executionForcedToKubernetes || + (environmentDriver !== "local" && + environmentDriver !== "ssh"))); + if (shouldSerialize) { + throw new WorkspaceBusyDeferral({ + holder: workspaceHolder, + projectWorkspaceId: issueRef.projectWorkspaceId, + deferralAttempt: + run.scheduledRetryReason === WORKSPACE_BUSY_RETRY_REASON + ? (run.scheduledRetryAttempt ?? 0) + : 0, + wasIssueAssignee: issueContext?.assigneeAgentId === agent.id, + }); + } - const holderIssueLabel = workspaceHolder.issueIdentifier ?? workspaceHolder.issueId; - const concurrentWorkspaceNote = - `shared workspace is concurrently held by run ${workspaceHolder.runId} (issue ${holderIssueLabel}); ` - + "expect concurrent mutations, coordinate via commits"; - const appendConcurrentWorkspaceNote = (value: unknown) => { - const existing = typeof value === "string" ? value.trimEnd() : ""; - return existing ? `${existing}\n${concurrentWorkspaceNote}` : concurrentWorkspaceNote; - }; - context.paperclipTaskMarkdown = appendConcurrentWorkspaceNote(context.paperclipTaskMarkdown); - if (typeof context.paperclipTaskMarkdownCompact === "string") { - context.paperclipTaskMarkdownCompact = appendConcurrentWorkspaceNote( - context.paperclipTaskMarkdownCompact, + const holderIssueLabel = + workspaceHolder.issueIdentifier ?? workspaceHolder.issueId; + const concurrentWorkspaceNote = + `shared workspace is concurrently held by run ${workspaceHolder.runId} (issue ${holderIssueLabel}); ` + + "expect concurrent mutations, coordinate via commits"; + const appendConcurrentWorkspaceNote = (value: unknown) => { + const existing = typeof value === "string" ? value.trimEnd() : ""; + return existing + ? `${existing}\n${concurrentWorkspaceNote}` + : concurrentWorkspaceNote; + }; + context.paperclipTaskMarkdown = appendConcurrentWorkspaceNote( + context.paperclipTaskMarkdown, + ); + if (typeof context.paperclipTaskMarkdownCompact === "string") { + context.paperclipTaskMarkdownCompact = + appendConcurrentWorkspaceNote( + context.paperclipTaskMarkdownCompact, + ); + } + logger.info( + { + event: "shared_workspace_concurrent_dispatch", + runId: run.id, + issueId: issueRef.id, + projectWorkspaceId: issueRef.projectWorkspaceId, + holderRunId: workspaceHolder.runId, + holderIssueId: workspaceHolder.issueId, + sharedWorkspaceConcurrency, + environmentDriver, + executionForcedToKubernetes, + }, + "Dispatching alongside a live shared-workspace holder", ); } - logger.info( + } + const workspaceManagedConfig = buildExecutionWorkspaceAdapterConfig({ + agentConfig: config, + projectPolicy: projectExecutionWorkspacePolicy, + issueSettings: issueExecutionWorkspaceSettings, + mode: requestedExecutionWorkspaceMode, + legacyUseProjectWorkspace: + issueAssigneeOverrides?.useProjectWorkspace ?? null, + }); + let adapterModelProfiles: AdapterModelProfileDefinition[] = []; + let profileResolutionFallbackReason: string | null = null; + try { + adapterModelProfiles = await listAdapterModelProfiles( + agent.adapterType, + ); + } catch (error) { + profileResolutionFallbackReason = "adapter_profile_resolution_failed"; + logger.warn( { - event: "shared_workspace_concurrent_dispatch", + err: error, + companyId: agent.companyId, + agentId: agent.id, + adapterType: agent.adapterType, runId: run.id, - issueId: issueRef.id, - projectWorkspaceId: issueRef.projectWorkspaceId, - holderRunId: workspaceHolder.runId, - holderIssueId: workspaceHolder.issueId, - sharedWorkspaceConcurrency, - environmentDriver, - executionForcedToKubernetes, }, - "Dispatching alongside a live shared-workspace holder", + "Failed to resolve adapter model profiles; falling back to primary adapter config", ); } - } - const workspaceManagedConfig = buildExecutionWorkspaceAdapterConfig({ - agentConfig: config, - projectPolicy: projectExecutionWorkspacePolicy, - issueSettings: issueExecutionWorkspaceSettings, - mode: requestedExecutionWorkspaceMode, - legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null, - }); - let adapterModelProfiles: AdapterModelProfileDefinition[] = []; - let profileResolutionFallbackReason: string | null = null; - try { - adapterModelProfiles = await listAdapterModelProfiles(agent.adapterType); - } catch (error) { - profileResolutionFallbackReason = "adapter_profile_resolution_failed"; - logger.warn( - { - err: error, + const modelProfileApplication = resolveModelProfileApplication({ + adapterModelProfiles, + agentRuntimeConfig: agent.runtimeConfig, + issueModelProfile: issueAssigneeOverrides?.modelProfile ?? null, + contextSnapshot: context, + profileResolutionFallbackReason, + }); + const modelProfileMetadata = modelProfileRunMetadata( + modelProfileApplication, + ); + if (modelProfileMetadata) { + context.paperclipModelProfile = modelProfileMetadata; + if (modelProfileApplication.requested) + context.modelProfile = modelProfileApplication.requested; + } else { + delete context.paperclipModelProfile; + } + const mergedConfig = mergeModelProfileAdapterConfig({ + baseConfig: workspaceManagedConfig, + modelProfile: modelProfileApplication, + issueAdapterConfig: issueAssigneeOverrides?.adapterConfig ?? null, + }); + const configSnapshot = buildExecutionWorkspaceConfigSnapshot( + mergedConfig, + selectedEnvironmentId, + ); + const executionRunConfig = + stripWorkspaceRuntimeFromExecutionRunConfig(mergedConfig); + const runScopedMentionedSkillKeys = + await resolveRunScopedMentionedSkillKeys({ + db, + companyId: agent.companyId, + issueId, + }); + const pushCapabilityPreflightRequired = requiresPushCapabilityPreflight({ + adapterType: agent.adapterType, + issueId, + explicitRunScopedSkillKeys: runScopedMentionedSkillKeys, + }); + const { resolvedConfig, secretKeys, secretManifest } = + await resolveExecutionRunAdapterConfig({ companyId: agent.companyId, agentId: agent.id, adapterType: agent.adapterType, - runId: run.id, - }, - "Failed to resolve adapter model profiles; falling back to primary adapter config", - ); - } - const modelProfileApplication = resolveModelProfileApplication({ - adapterModelProfiles, - agentRuntimeConfig: agent.runtimeConfig, - issueModelProfile: issueAssigneeOverrides?.modelProfile ?? null, - contextSnapshot: context, - profileResolutionFallbackReason, - }); - const modelProfileMetadata = modelProfileRunMetadata(modelProfileApplication); - if (modelProfileMetadata) { - context.paperclipModelProfile = modelProfileMetadata; - if (modelProfileApplication.requested) context.modelProfile = modelProfileApplication.requested; - } else { - delete context.paperclipModelProfile; - } - const mergedConfig = mergeModelProfileAdapterConfig({ - baseConfig: workspaceManagedConfig, - modelProfile: modelProfileApplication, - issueAdapterConfig: issueAssigneeOverrides?.adapterConfig ?? null, - }); - const configSnapshot = buildExecutionWorkspaceConfigSnapshot(mergedConfig, selectedEnvironmentId); - const executionRunConfig = stripWorkspaceRuntimeFromExecutionRunConfig(mergedConfig); - const runScopedMentionedSkillKeys = await resolveRunScopedMentionedSkillKeys({ - db, - companyId: agent.companyId, - issueId, - }); - const pushCapabilityPreflightRequired = requiresPushCapabilityPreflight({ - adapterType: agent.adapterType, - issueId, - explicitRunScopedSkillKeys: runScopedMentionedSkillKeys, - }); - const { resolvedConfig, secretKeys, secretManifest } = await resolveExecutionRunAdapterConfig({ - companyId: agent.companyId, - agentId: agent.id, - adapterType: agent.adapterType, - issueId, - heartbeatRunId: run.id, - environmentId: selectedEnvironmentForConfig?.id ?? null, - environmentEnv: selectedEnvironmentForConfig?.envVars ?? null, - environmentDriver: selectedEnvironmentForConfig?.driver ?? null, - projectId: projectContext?.id ?? null, - routineId: routineEnvContext.routineId, - responsibleUserId, - executionRunConfig, - projectEnv: projectContext?.env ?? null, - routineEnv: routineEnvContext.env, - secretsSvc, - trustPreset, - requiredScopedEnvBinding: pushCapabilityPreflightRequired - ? { - keys: [...PUSH_CAPABILITY_ENV_KEYS], - consumerScopes: ["agent", "project"], - reason: "push_write_credential_missing", - remediation: - "GitHub PR workflow requires GH_TOKEN or GITHUB_TOKEN bound at project or agent scope.", - } - : undefined, - }); - if (secretManifest.length > 0) { - context.paperclipSecrets = { - manifest: secretManifest, - }; - } else { - delete context.paperclipSecrets; - } - const effectiveResolvedConfig = applyRunScopedMentionedSkillKeys( - resolvedConfig, - runScopedMentionedSkillKeys, - ); - const runtimeSkillPreference = readPaperclipSkillSyncPreference(effectiveResolvedConfig); - const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(agent.companyId, { - versionSelections: skillVersionSelectionMap(runtimeSkillPreference.desiredSkillEntries, { - versionPinsEnabled: resolvedInstanceSettings.experimental.enableBetaSkills === true, - }), - }); - let runtimeConfig: Record = { - ...effectiveResolvedConfig, - paperclipRuntimeSkills: runtimeSkillEntries, - }; - const latestAgentConfigRevision = await getLatestAgentConfigRevision(agent.companyId, agent.id); - const sessionConfigMetadata = await buildEffectiveRunSessionConfigMetadata({ - adapterType: agent.adapterType, - effectiveAdapterConfig: runtimeConfig, - agentRuntimeConfig: agent.runtimeConfig, - modelProfile: modelProfileMetadata, - issueOverrides: issueAssigneeOverrides, - workspaceConfig: { - requestedMode: requestedExecutionWorkspaceMode, - effectiveMode: effectiveExecutionWorkspaceMode, - issueConfigRevisionAt: issueContext?.updatedAt instanceof Date - ? issueContext.updatedAt.toISOString() - : issueContext?.updatedAt ?? null, - projectConfigRevisionAt: projectContext?.updatedAt instanceof Date - ? projectContext.updatedAt.toISOString() - : projectContext?.updatedAt ?? null, - projectPolicy: projectExecutionWorkspacePolicy, - issueSettings: issueExecutionWorkspaceSettings, - reusableExecutionWorkspaceConfig: requestedReusableExecutionWorkspaceConfig, - existingExecutionWorkspace: reusableExistingExecutionWorkspace - ? { - id: reusableExistingExecutionWorkspace.id, - mode: reusableExistingExecutionWorkspace.mode, - strategyType: reusableExistingExecutionWorkspace.strategyType, - projectWorkspaceId: reusableExistingExecutionWorkspace.projectWorkspaceId, - repoUrl: reusableExistingExecutionWorkspace.repoUrl, - baseRef: reusableExistingExecutionWorkspace.baseRef, - branchName: reusableExistingExecutionWorkspace.branchName, - config: reusableExistingExecutionWorkspace.config, - } - : null, - }, - environment: { - selectionSource: environmentResolution.source, - selectedEnvironmentId, - selectedEnvironment: selectedEnvironmentForConfig - ? { - id: selectedEnvironmentForConfig.id, - driver: selectedEnvironmentForConfig.driver, - config: selectedEnvironmentForConfig.config, - configRevisionAt: selectedEnvironmentForConfig.updatedAt instanceof Date - ? selectedEnvironmentForConfig.updatedAt.toISOString() - : selectedEnvironmentForConfig.updatedAt ?? null, - } - : null, - executionPolicy, - }, - environmentEnv: selectedEnvironmentForConfig?.envVars ?? null, - projectEnv: projectContext?.env ?? null, - routineEnv: routineEnvContext.env, - secretManifest, - runtimeSkills: runtimeSkillEntries, - agentConfigRevision: latestAgentConfigRevision - ? { - id: latestAgentConfigRevision.id, - changedKeys: latestAgentConfigRevision.changedKeys, - configRevisionAt: latestAgentConfigRevision.createdAt.toISOString(), - } - : null, - }); - const configuredModel = readConfiguredModelFromAdapterConfig(runtimeConfig); - const wakeSessionResetReason = describeSessionResetReason(context); - const sessionConfigFreshness = resolveTaskSessionConfigFreshness({ - hasTaskSession: taskSession != null, - configuredModel, - taskSessionParams: taskSession?.sessionParamsJson ?? taskSessionDecodedParams, - configMetadata: sessionConfigMetadata, - wakeResetReason: wakeSessionResetReason, - preserveLegacySessionWithoutConfigMetadata: acceptedPlanContinuationWake && !acceptedPlanWakeRoutingDecision, - }); - const resetTaskSession = shouldResetTaskSessionForWake(context) || sessionConfigFreshness.reset; - const sessionResetReason = sessionConfigFreshness.reasons.join("; ") || null; - const taskSessionForRun = resetTaskSession ? null : taskSession; - const previousSessionParams = - explicitResumeSessionParams ?? - (isCanonicalSessionIdForAdapter(agent.adapterType, explicitResumeSessionDisplayId) - ? { sessionId: explicitResumeSessionDisplayId } - : null) ?? - normalizeResumeParamsForAdapter( - agent.adapterType, - stripPaperclipSessionMetadataFromSessionParams( - sessionCodec.deserialize(taskSessionForRun?.sessionParamsJson ?? null), - ), - ); - const { - selectedEnvironmentDriver: lowTrustPreflightEnvironmentDriver, - workspace: resolvedWorkspace, - } = await resolveWorkspaceAfterLowTrustPreflight({ - db, - trustPreset, - isolatedWorkspacesEnabled, - effectiveExecutionWorkspaceMode, - issue: issueRef - ? { - companyId: agent.companyId, - id: issueRef.id, - projectId: issueRef.projectId, - } - : null, - resolveSelectedEnvironmentDriver: async () => { - const preflightEnvironment = await envOrchestrator.resolveEnvironment({ - companyId: agent.companyId, - selectedEnvironmentId, - localEnvironmentId: localEnvironment.id, + issueId, + heartbeatRunId: run.id, + environmentId: selectedEnvironmentForConfig?.id ?? null, + environmentEnv: selectedEnvironmentForConfig?.envVars ?? null, + environmentDriver: selectedEnvironmentForConfig?.driver ?? null, + projectId: projectContext?.id ?? null, + routineId: routineEnvContext.routineId, + responsibleUserId, + executionRunConfig, + projectEnv: projectContext?.env ?? null, + routineEnv: routineEnvContext.env, + secretsSvc, + trustPreset, + requiredScopedEnvBinding: pushCapabilityPreflightRequired + ? { + keys: [...PUSH_CAPABILITY_ENV_KEYS], + consumerScopes: ["agent", "project"], + reason: "push_write_credential_missing", + remediation: + "GitHub PR workflow requires GH_TOKEN or GITHUB_TOKEN bound at project or agent scope.", + } + : undefined, }); - return preflightEnvironment.driver; - }, - resolveWorkspace: () => - resolveWorkspaceForRun( - agent, - context, - previousSessionParams, - { - useProjectWorkspace: requestedExecutionWorkspaceMode !== "agent_default", + if (secretManifest.length > 0) { + context.paperclipSecrets = { + manifest: secretManifest, + }; + } else { + delete context.paperclipSecrets; + } + const effectiveResolvedConfig = applyRunScopedMentionedSkillKeys( + resolvedConfig, + runScopedMentionedSkillKeys, + ); + const runtimeSkillPreference = readPaperclipSkillSyncPreference( + effectiveResolvedConfig, + ); + const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries( + agent.companyId, + { + versionSelections: skillVersionSelectionMap( + runtimeSkillPreference.desiredSkillEntries, + { + versionPinsEnabled: + resolvedInstanceSettings.experimental.enableBetaSkills === true, + }, + ), + }, + ); + let runtimeConfig: Record = { + ...effectiveResolvedConfig, + paperclipRuntimeSkills: runtimeSkillEntries, + }; + const latestAgentConfigRevision = await getLatestAgentConfigRevision( + agent.companyId, + agent.id, + ); + const sessionConfigMetadata = + await buildEffectiveRunSessionConfigMetadata({ + adapterType: agent.adapterType, + effectiveAdapterConfig: runtimeConfig, + agentRuntimeConfig: agent.runtimeConfig, + modelProfile: modelProfileMetadata, + issueOverrides: issueAssigneeOverrides, + workspaceConfig: { + requestedMode: requestedExecutionWorkspaceMode, + effectiveMode: effectiveExecutionWorkspaceMode, + issueConfigRevisionAt: + issueContext?.updatedAt instanceof Date + ? issueContext.updatedAt.toISOString() + : (issueContext?.updatedAt ?? null), + projectConfigRevisionAt: + projectContext?.updatedAt instanceof Date + ? projectContext.updatedAt.toISOString() + : (projectContext?.updatedAt ?? null), + projectPolicy: projectExecutionWorkspacePolicy, + issueSettings: issueExecutionWorkspaceSettings, + reusableExecutionWorkspaceConfig: + requestedReusableExecutionWorkspaceConfig, + existingExecutionWorkspace: reusableExistingExecutionWorkspace + ? { + id: reusableExistingExecutionWorkspace.id, + mode: reusableExistingExecutionWorkspace.mode, + strategyType: reusableExistingExecutionWorkspace.strategyType, + projectWorkspaceId: + reusableExistingExecutionWorkspace.projectWorkspaceId, + repoUrl: reusableExistingExecutionWorkspace.repoUrl, + baseRef: reusableExistingExecutionWorkspace.baseRef, + branchName: reusableExistingExecutionWorkspace.branchName, + config: reusableExistingExecutionWorkspace.config, + } + : null, + }, + environment: { + selectionSource: environmentResolution.source, + selectedEnvironmentId, + selectedEnvironment: selectedEnvironmentForConfig + ? { + id: selectedEnvironmentForConfig.id, + driver: selectedEnvironmentForConfig.driver, + config: selectedEnvironmentForConfig.config, + configRevisionAt: + selectedEnvironmentForConfig.updatedAt instanceof Date + ? selectedEnvironmentForConfig.updatedAt.toISOString() + : (selectedEnvironmentForConfig.updatedAt ?? null), + } + : null, + executionPolicy, + }, + environmentEnv: selectedEnvironmentForConfig?.envVars ?? null, + projectEnv: projectContext?.env ?? null, + routineEnv: routineEnvContext.env, + secretManifest, + runtimeSkills: runtimeSkillEntries, + agentConfigRevision: latestAgentConfigRevision + ? { + id: latestAgentConfigRevision.id, + changedKeys: latestAgentConfigRevision.changedKeys, + configRevisionAt: + latestAgentConfigRevision.createdAt.toISOString(), + } + : null, + }); + const configuredModel = + readConfiguredModelFromAdapterConfig(runtimeConfig); + const wakeSessionResetReason = describeSessionResetReason(context); + const sessionConfigFreshness = resolveTaskSessionConfigFreshness({ + hasTaskSession: taskSession != null, + configuredModel, + taskSessionParams: + taskSession?.sessionParamsJson ?? taskSessionDecodedParams, + configMetadata: sessionConfigMetadata, + wakeResetReason: wakeSessionResetReason, + preserveLegacySessionWithoutConfigMetadata: + acceptedPlanContinuationWake && !acceptedPlanWakeRoutingDecision, + }); + const resetTaskSession = + shouldResetTaskSessionForWake(context) || sessionConfigFreshness.reset; + const sessionResetReason = + sessionConfigFreshness.reasons.join("; ") || null; + const taskSessionForRun = resetTaskSession ? null : taskSession; + const previousSessionParams = + explicitResumeSessionParams ?? + (isCanonicalSessionIdForAdapter( + agent.adapterType, + explicitResumeSessionDisplayId, + ) + ? { sessionId: explicitResumeSessionDisplayId } + : null) ?? + normalizeResumeParamsForAdapter( + agent.adapterType, + stripPaperclipSessionMetadataFromSessionParams( + sessionCodec.deserialize( + taskSessionForRun?.sessionParamsJson ?? null, + ), + ), + ); + const { + selectedEnvironmentDriver: lowTrustPreflightEnvironmentDriver, + workspace: resolvedWorkspace, + } = await resolveWorkspaceAfterLowTrustPreflight({ + db, + trustPreset, + isolatedWorkspacesEnabled, + effectiveExecutionWorkspaceMode, + issue: issueRef + ? { + companyId: agent.companyId, + id: issueRef.id, + projectId: issueRef.projectId, + } + : null, + resolveSelectedEnvironmentDriver: async () => { + const preflightEnvironment = await envOrchestrator.resolveEnvironment( + { + companyId: agent.companyId, + selectedEnvironmentId, + localEnvironmentId: localEnvironment.id, + }, + ); + return preflightEnvironment.driver; + }, + resolveWorkspace: () => + resolveWorkspaceForRun(agent, context, previousSessionParams, { + useProjectWorkspace: + requestedExecutionWorkspaceMode !== "agent_default", // Thread the selected environment driver so run-workspace resolution can tell a local // target from a remote one, and a confined sandbox target from an unconfined remote // target. A remote run resolves referenced projects only for the confined sandbox // transport with the remote flag on. This never changes the anchor workspace. - executionEnvironmentDriver: selectedEnvironmentForConfig?.driver ?? null, - }, + executionEnvironmentDriver: + selectedEnvironmentForConfig?.driver ?? null, + }), + }); + const hostExecutionWorkspaceConfig = + stripHostWorkspaceProvisionForLowTrustSandbox({ + config: mergedConfig, + trustPreset, + selectedEnvironmentDriver: lowTrustPreflightEnvironmentDriver, + }); + const executionWorkspaceBase = { + baseCwd: resolvedWorkspace.cwd, + source: resolvedWorkspace.source, + projectId: resolvedWorkspace.projectId, + workspaceId: resolvedWorkspace.workspaceId, + repoUrl: resolvedWorkspace.repoUrl, + repoRef: resolvedWorkspace.repoRef, + additionalWorkspaces: resolvedWorkspace.additionalWorkspaces, + } satisfies ExecutionWorkspaceInput; + await assertGitWorktreeBaseWorkspaceReady({ + requestedExecutionWorkspaceMode, + config: hostExecutionWorkspaceConfig, + issue: issueRef, + base: executionWorkspaceBase, + anchor: { + baseCwdFallback: resolvedWorkspace.baseCwdFallback, + materializationFailures: resolvedWorkspace.materializationFailures, + }, + }); + const workspaceStrategyForFingerprint = parseObject( + hostExecutionWorkspaceConfig.workspaceStrategy, + ); + const workspaceStrategyFingerprintValue = + Object.keys(workspaceStrategyForFingerprint).length > 0 + ? workspaceStrategyForFingerprint + : null; + const latestWorkspaceStrategyType = resolveEffectiveWorkspaceStrategyType( + requestedExecutionWorkspaceMode, + hostExecutionWorkspaceConfig, + ); + const selectedEnvironmentConfigForFingerprint = parseObject( + selectedEnvironmentForConfig?.config, + ); + const workspaceEnvironmentFingerprint = selectedEnvironmentForConfig + ? { + selectionSource: environmentResolution.source, + selectedEnvironmentId, + driver: selectedEnvironmentForConfig.driver, + provider: readNonEmptyString( + selectedEnvironmentConfigForFingerprint.provider, + ), + config: selectedEnvironmentForConfig.config, + configRevisionAt: + selectedEnvironmentForConfig.updatedAt instanceof Date + ? selectedEnvironmentForConfig.updatedAt.toISOString() + : (selectedEnvironmentForConfig.updatedAt ?? null), + executionPolicy, + } + : null; + const workspaceRealizationFingerprint = { + environmentDriver: selectedEnvironmentForConfig?.driver ?? null, + environmentProvider: readNonEmptyString( + selectedEnvironmentConfigForFingerprint.provider, ), - }); - const hostExecutionWorkspaceConfig = stripHostWorkspaceProvisionForLowTrustSandbox({ - config: mergedConfig, - trustPreset, - selectedEnvironmentDriver: lowTrustPreflightEnvironmentDriver, - }); - const executionWorkspaceBase = { - baseCwd: resolvedWorkspace.cwd, - source: resolvedWorkspace.source, - projectId: resolvedWorkspace.projectId, - workspaceId: resolvedWorkspace.workspaceId, - repoUrl: resolvedWorkspace.repoUrl, - repoRef: resolvedWorkspace.repoRef, - additionalWorkspaces: resolvedWorkspace.additionalWorkspaces, - } satisfies ExecutionWorkspaceInput; - await assertGitWorktreeBaseWorkspaceReady({ - requestedExecutionWorkspaceMode, - config: hostExecutionWorkspaceConfig, - issue: issueRef, - base: executionWorkspaceBase, - anchor: { - baseCwdFallback: resolvedWorkspace.baseCwdFallback, - materializationFailures: resolvedWorkspace.materializationFailures, - }, - }); - const workspaceStrategyForFingerprint = parseObject(hostExecutionWorkspaceConfig.workspaceStrategy); - const workspaceStrategyFingerprintValue = - Object.keys(workspaceStrategyForFingerprint).length > 0 ? workspaceStrategyForFingerprint : null; - const latestWorkspaceStrategyType = resolveEffectiveWorkspaceStrategyType( - requestedExecutionWorkspaceMode, - hostExecutionWorkspaceConfig, - ); - const selectedEnvironmentConfigForFingerprint = parseObject(selectedEnvironmentForConfig?.config); - const workspaceEnvironmentFingerprint = selectedEnvironmentForConfig - ? { - selectionSource: environmentResolution.source, - selectedEnvironmentId, - driver: selectedEnvironmentForConfig.driver, - provider: readNonEmptyString(selectedEnvironmentConfigForFingerprint.provider), - config: selectedEnvironmentForConfig.config, - configRevisionAt: selectedEnvironmentForConfig.updatedAt instanceof Date - ? selectedEnvironmentForConfig.updatedAt.toISOString() - : selectedEnvironmentForConfig.updatedAt ?? null, - executionPolicy, - } - : null; - const workspaceRealizationFingerprint = { - environmentDriver: selectedEnvironmentForConfig?.driver ?? null, - environmentProvider: readNonEmptyString(selectedEnvironmentConfigForFingerprint.provider), - trustPreset: trustPreset.kind, - lowTrustSandboxDriver: lowTrustPreflightEnvironmentDriver, - }; - const latestWorkspaceConfigMetadata = buildEffectiveRunWorkspaceConfigMetadata({ - mode: requestedExecutionWorkspaceMode, - projectId: executionWorkspaceBase.projectId, - projectWorkspaceId: executionWorkspaceBase.workspaceId, - strategyType: latestWorkspaceStrategyType, - workspaceStrategy: workspaceStrategyFingerprintValue, - repoUrl: executionWorkspaceBase.repoUrl, - repoRef: readNonEmptyString(workspaceStrategyForFingerprint.baseRef) ?? executionWorkspaceBase.repoRef, - configSnapshot, - environment: workspaceEnvironmentFingerprint, - realization: workspaceRealizationFingerprint, - secretManifest, - }); - const inferredExistingWorkspaceConfigMetadata = reusableExistingExecutionWorkspace - ? buildEffectiveRunWorkspaceConfigMetadata({ - mode: issueExecutionWorkspaceModeForPersistedWorkspace(reusableExistingExecutionWorkspace.mode), - projectId: reusableExistingExecutionWorkspace.projectId, - projectWorkspaceId: reusableExistingExecutionWorkspace.projectWorkspaceId, - strategyType: reusableExistingExecutionWorkspace.strategyType, - workspaceStrategy: workspaceStrategyFingerprintValue - ? { - ...workspaceStrategyFingerprintValue, - type: reusableExistingExecutionWorkspace.strategyType, - ...(reusableExistingExecutionWorkspace.baseRef - ? { baseRef: reusableExistingExecutionWorkspace.baseRef } - : {}), - } - : { type: reusableExistingExecutionWorkspace.strategyType }, - repoUrl: reusableExistingExecutionWorkspace.repoUrl, - repoRef: reusableExistingExecutionWorkspace.baseRef, - configSnapshot: reusableExistingExecutionWorkspace.config, + trustPreset: trustPreset.kind, + lowTrustSandboxDriver: lowTrustPreflightEnvironmentDriver, + }; + const latestWorkspaceConfigMetadata = + buildEffectiveRunWorkspaceConfigMetadata({ + mode: requestedExecutionWorkspaceMode, + projectId: executionWorkspaceBase.projectId, + projectWorkspaceId: executionWorkspaceBase.workspaceId, + strategyType: latestWorkspaceStrategyType, + workspaceStrategy: workspaceStrategyFingerprintValue, + repoUrl: executionWorkspaceBase.repoUrl, + repoRef: + readNonEmptyString(workspaceStrategyForFingerprint.baseRef) ?? + executionWorkspaceBase.repoRef, + configSnapshot, environment: workspaceEnvironmentFingerprint, realization: workspaceRealizationFingerprint, secretManifest, - evaluatedAt: latestWorkspaceConfigMetadata.evaluatedAt, - }) - : null; - const workspaceConfigFreshness = resolveExecutionWorkspaceConfigFreshness({ - hasExistingWorkspace: requestedShouldReuseExisting && Boolean(reusableExistingExecutionWorkspace), - existingWorkspaceMetadata: reusableExistingExecutionWorkspace?.metadata ?? null, - inferredMetadata: inferredExistingWorkspaceConfigMetadata, - nextMetadata: latestWorkspaceConfigMetadata, - }); - const workspaceReuseProvisioningPolicy = resolveExecutionWorkspaceReuseProvisioningPolicy({ - requestedShouldReuseExisting, - workspaceConfigFreshness, - }); - const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({ - companyId: agent.companyId, - heartbeatRunId: run.id, - executionWorkspaceId: workspaceReuseProvisioningPolicy.shouldRestoreExistingWorkspace - ? workspaceReuseRequest.requestedExecutionWorkspaceId - : null, - issueId, - }); - // One credential provider per run: base-ref refreshes during workspace realization and - // restore authenticate against private GitHub remotes with the same company-secret token - // the managed clone uses. - const workspaceGitAuthProvider = createGitRemoteAuthProvider(db, agent.companyId, { - issueId, - heartbeatRunId: run.id, - }); - const { executionWorkspace, reusedExecutionWorkspace, policy: resolvedWorkspaceReusePolicy } = - await provisionExecutionWorkspaceForFreshnessDecision({ - requestedShouldReuseExisting, - existingExecutionWorkspaceId: workspaceReuseRequest.requestedExecutionWorkspaceId, - issueRef, - runId: run.id, - workspaceConfigFreshness, - restoreExistingWorkspace: reusableExistingExecutionWorkspace - ? () => ensurePersistedExecutionWorkspaceAvailable({ + }); + const inferredExistingWorkspaceConfigMetadata = + reusableExistingExecutionWorkspace + ? buildEffectiveRunWorkspaceConfigMetadata({ + mode: issueExecutionWorkspaceModeForPersistedWorkspace( + reusableExistingExecutionWorkspace.mode, + ), + projectId: reusableExistingExecutionWorkspace.projectId, + projectWorkspaceId: + reusableExistingExecutionWorkspace.projectWorkspaceId, + strategyType: reusableExistingExecutionWorkspace.strategyType, + workspaceStrategy: workspaceStrategyFingerprintValue + ? { + ...workspaceStrategyFingerprintValue, + type: reusableExistingExecutionWorkspace.strategyType, + ...(reusableExistingExecutionWorkspace.baseRef + ? { baseRef: reusableExistingExecutionWorkspace.baseRef } + : {}), + } + : { type: reusableExistingExecutionWorkspace.strategyType }, + repoUrl: reusableExistingExecutionWorkspace.repoUrl, + repoRef: reusableExistingExecutionWorkspace.baseRef, + configSnapshot: reusableExistingExecutionWorkspace.config, + environment: workspaceEnvironmentFingerprint, + realization: workspaceRealizationFingerprint, + secretManifest, + evaluatedAt: latestWorkspaceConfigMetadata.evaluatedAt, + }) + : null; + const workspaceConfigFreshness = resolveExecutionWorkspaceConfigFreshness( + { + hasExistingWorkspace: + requestedShouldReuseExisting && + Boolean(reusableExistingExecutionWorkspace), + existingWorkspaceMetadata: + reusableExistingExecutionWorkspace?.metadata ?? null, + inferredMetadata: inferredExistingWorkspaceConfigMetadata, + nextMetadata: latestWorkspaceConfigMetadata, + }, + ); + const workspaceReuseProvisioningPolicy = + resolveExecutionWorkspaceReuseProvisioningPolicy({ + requestedShouldReuseExisting, + workspaceConfigFreshness, + }); + const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({ + companyId: agent.companyId, + heartbeatRunId: run.id, + executionWorkspaceId: + workspaceReuseProvisioningPolicy.shouldRestoreExistingWorkspace + ? workspaceReuseRequest.requestedExecutionWorkspaceId + : null, + issueId, + }); + // One credential provider per run: base-ref refreshes during workspace realization and + // restore authenticate against private GitHub remotes with the same company-secret token + // the managed clone uses. + const workspaceGitAuthProvider = createGitRemoteAuthProvider( + db, + agent.companyId, + { + issueId, + heartbeatRunId: run.id, + }, + ); + const { + executionWorkspace, + reusedExecutionWorkspace, + policy: resolvedWorkspaceReusePolicy, + } = await provisionExecutionWorkspaceForFreshnessDecision( + { + requestedShouldReuseExisting, + existingExecutionWorkspaceId: + workspaceReuseRequest.requestedExecutionWorkspaceId, + issueRef, + runId: run.id, + workspaceConfigFreshness, + restoreExistingWorkspace: reusableExistingExecutionWorkspace + ? () => + ensurePersistedExecutionWorkspaceAvailable({ + db, + base: executionWorkspaceBase, + workspace: { + id: reusableExistingExecutionWorkspace.id, + mode: reusableExistingExecutionWorkspace.mode, + strategyType: + reusableExistingExecutionWorkspace.strategyType, + cwd: reusableExistingExecutionWorkspace.cwd, + providerRef: reusableExistingExecutionWorkspace.providerRef, + projectId: reusableExistingExecutionWorkspace.projectId, + projectWorkspaceId: + reusableExistingExecutionWorkspace.projectWorkspaceId, + repoUrl: reusableExistingExecutionWorkspace.repoUrl, + baseRef: reusableExistingExecutionWorkspace.baseRef, + branchName: reusableExistingExecutionWorkspace.branchName, + metadata: + reusableExistingExecutionWorkspace.metadata as Record< + string, + unknown + > | null, + config: { + provisionCommand: + configSnapshot?.provisionCommand ?? + reusableExistingExecutionWorkspace.config + ?.provisionCommand ?? + projectExecutionWorkspacePolicy?.workspaceStrategy + ?.provisionCommand ?? + null, + runtimeProvisionCommand: + configSnapshot?.runtimeProvisionCommand ?? + reusableExistingExecutionWorkspace.config + ?.runtimeProvisionCommand ?? + projectExecutionWorkspacePolicy?.workspaceStrategy + ?.runtimeProvisionCommand ?? + null, + }, + }, + issue: issueRef, + agent: { + id: agent.id, + name: agent.name, + companyId: agent.companyId, + }, + heartbeatRunId: run.id, + enableWorkspaceBranchReconcileForward: + resolvedInstanceSettings.experimental + .enableWorkspaceBranchReconcileForward, + enableWorkspaceDirtyQuarantineRepair: + resolvedInstanceSettings.experimental + .enableWorkspaceDirtyQuarantineRepair, + recorder: workspaceOperationRecorder, + resolveGitAuth: workspaceGitAuthProvider, + }) + : null, + realizeWorkspace: () => + realizeExecutionWorkspace({ db, base: executionWorkspaceBase, - workspace: { - id: reusableExistingExecutionWorkspace.id, - mode: reusableExistingExecutionWorkspace.mode, - strategyType: reusableExistingExecutionWorkspace.strategyType, - cwd: reusableExistingExecutionWorkspace.cwd, - providerRef: reusableExistingExecutionWorkspace.providerRef, - projectId: reusableExistingExecutionWorkspace.projectId, - projectWorkspaceId: reusableExistingExecutionWorkspace.projectWorkspaceId, - repoUrl: reusableExistingExecutionWorkspace.repoUrl, - baseRef: reusableExistingExecutionWorkspace.baseRef, - branchName: reusableExistingExecutionWorkspace.branchName, - metadata: reusableExistingExecutionWorkspace.metadata as Record | null, - config: { - provisionCommand: - configSnapshot?.provisionCommand - ?? reusableExistingExecutionWorkspace.config?.provisionCommand - ?? projectExecutionWorkspacePolicy?.workspaceStrategy?.provisionCommand - ?? null, - runtimeProvisionCommand: - configSnapshot?.runtimeProvisionCommand - ?? reusableExistingExecutionWorkspace.config?.runtimeProvisionCommand - ?? projectExecutionWorkspacePolicy?.workspaceStrategy?.runtimeProvisionCommand - ?? null, - }, - }, + config: hostExecutionWorkspaceConfig, issue: issueRef, agent: { id: agent.id, @@ -15031,797 +17454,273 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }, heartbeatRunId: run.id, enableWorkspaceBranchReconcileForward: - resolvedInstanceSettings.experimental.enableWorkspaceBranchReconcileForward, + resolvedInstanceSettings.experimental + .enableWorkspaceBranchReconcileForward, enableWorkspaceDirtyQuarantineRepair: - resolvedInstanceSettings.experimental.enableWorkspaceDirtyQuarantineRepair, + resolvedInstanceSettings.experimental + .enableWorkspaceDirtyQuarantineRepair, recorder: workspaceOperationRecorder, resolveGitAuth: workspaceGitAuthProvider, - }) - : null, - realizeWorkspace: () => realizeExecutionWorkspace({ - db, - base: executionWorkspaceBase, - config: hostExecutionWorkspaceConfig, - issue: issueRef, - agent: { - id: agent.id, - name: agent.name, - companyId: agent.companyId, - }, - heartbeatRunId: run.id, - enableWorkspaceBranchReconcileForward: - resolvedInstanceSettings.experimental.enableWorkspaceBranchReconcileForward, - enableWorkspaceDirtyQuarantineRepair: - resolvedInstanceSettings.experimental.enableWorkspaceDirtyQuarantineRepair, - recorder: workspaceOperationRecorder, - resolveGitAuth: workspaceGitAuthProvider, - }), - }); - const resolvedProjectId = executionWorkspace.projectId ?? issueRef?.projectId ?? executionProjectId ?? null; - const resolvedProjectWorkspaceId = issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null; - let persistedExecutionWorkspace: ExecutionWorkspace | null = null; - const baseExecutionWorkspaceMetadata = mergeExecutionWorkspaceMetadataForPersistence({ - existingMetadata: resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace - ? reusableExistingExecutionWorkspace?.metadata ?? null - : null, - source: executionWorkspace.source, - createdByRuntime: executionWorkspace.created, - configSnapshot, - shouldReuseExisting: resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace, - shouldRefreshConfigSnapshot: resolvedWorkspaceReusePolicy.shouldRefreshWorkspaceConfigSnapshot, - workspaceConfigMetadata: resolvedWorkspaceReusePolicy.shouldPersistLatestWorkspaceConfigMetadata - ? latestWorkspaceConfigMetadata - : null, - baseRef: executionWorkspace.repoRef, - baseRefSha: executionWorkspace.baseRefSha ?? null, - }); - let persistedWorktreeInstanceRoot = - resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace - && typeof reusableExistingExecutionWorkspace?.metadata?.[WORKTREE_INSTANCE_ROOT_METADATA_KEY] === "string" - ? reusableExistingExecutionWorkspace.metadata[WORKTREE_INSTANCE_ROOT_METADATA_KEY] - : null; - if ( - !persistedWorktreeInstanceRoot - && executionWorkspace.strategy === "git_worktree" - && executionWorkspace.worktreePath - ) { - try { - persistedWorktreeInstanceRoot = ( - await readManagedWorktreeInstanceOwnership(executionWorkspace.worktreePath) - )?.instanceRoot ?? null; - } catch (error) { - logger.warn( - { - runId: run.id, - issueId, - executionWorkspaceCwd: executionWorkspace.cwd, - error: error instanceof Error ? error.message : String(error), - }, - "Could not record managed worktree instance ownership", - ); - } - } - const nextExecutionWorkspaceMetadata = { - ...baseExecutionWorkspaceMetadata, - ...(persistedWorktreeInstanceRoot - ? { [WORKTREE_INSTANCE_ROOT_METADATA_KEY]: persistedWorktreeInstanceRoot } - : {}), - }; - const pendingForwardBranchReconcile = executionWorkspace.pendingForwardBranchReconcile ?? null; - const branchNameForInitialPersistence = - pendingForwardBranchReconcile?.recordedBranchName ?? executionWorkspace.branchName; - try { - persistedExecutionWorkspace = resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace && reusableExistingExecutionWorkspace - ? await executionWorkspacesSvc.update(reusableExistingExecutionWorkspace.id, { - cwd: executionWorkspace.cwd, - repoUrl: executionWorkspace.repoUrl, - baseRef: executionWorkspace.repoRef, - branchName: branchNameForInitialPersistence, - providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs", - providerRef: executionWorkspace.worktreePath, - status: "active", - lastUsedAt: new Date(), - metadata: nextExecutionWorkspaceMetadata, - projectWorkspaceId: reconcileReusedExecutionWorkspaceProjectWorkspaceId( - reusableExistingExecutionWorkspace.projectWorkspaceId, - resolvedProjectWorkspaceId, - ), - }) - : resolvedProjectId - ? await executionWorkspacesSvc.create({ - companyId: agent.companyId, - projectId: resolvedProjectId, - projectWorkspaceId: resolvedProjectWorkspaceId, - sourceIssueId: issueRef?.id ?? null, - mode: - requestedExecutionWorkspaceMode === "isolated_workspace" - ? "isolated_workspace" - : requestedExecutionWorkspaceMode === "operator_branch" - ? "operator_branch" - : requestedExecutionWorkspaceMode === "agent_default" - ? "adapter_managed" - : "shared_workspace", - strategyType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "project_primary", - name: branchNameForInitialPersistence ?? issueRef?.identifier ?? `workspace-${agent.id.slice(0, 8)}`, - status: "active", - cwd: executionWorkspace.cwd, - repoUrl: executionWorkspace.repoUrl, - baseRef: executionWorkspace.repoRef, - branchName: branchNameForInitialPersistence, - providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs", - providerRef: executionWorkspace.worktreePath, - lastUsedAt: new Date(), - openedAt: new Date(), - metadata: nextExecutionWorkspaceMetadata, - }) + }), + }, + ); + const resolvedProjectId = + executionWorkspace.projectId ?? + issueRef?.projectId ?? + executionProjectId ?? + null; + const resolvedProjectWorkspaceId = + issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null; + let persistedExecutionWorkspace: ExecutionWorkspace | null = null; + const baseExecutionWorkspaceMetadata = + mergeExecutionWorkspaceMetadataForPersistence({ + existingMetadata: + resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace + ? (reusableExistingExecutionWorkspace?.metadata ?? null) + : null, + source: executionWorkspace.source, + createdByRuntime: executionWorkspace.created, + configSnapshot, + shouldReuseExisting: + resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace, + shouldRefreshConfigSnapshot: + resolvedWorkspaceReusePolicy.shouldRefreshWorkspaceConfigSnapshot, + workspaceConfigMetadata: + resolvedWorkspaceReusePolicy.shouldPersistLatestWorkspaceConfigMetadata + ? latestWorkspaceConfigMetadata + : null, + baseRef: executionWorkspace.repoRef, + baseRefSha: executionWorkspace.baseRefSha ?? null, + }); + let persistedWorktreeInstanceRoot = + resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace && + typeof reusableExistingExecutionWorkspace?.metadata?.[ + WORKTREE_INSTANCE_ROOT_METADATA_KEY + ] === "string" + ? reusableExistingExecutionWorkspace.metadata[ + WORKTREE_INSTANCE_ROOT_METADATA_KEY + ] : null; - } catch (error) { - if (executionWorkspace.created) { + if ( + !persistedWorktreeInstanceRoot && + executionWorkspace.strategy === "git_worktree" && + executionWorkspace.worktreePath + ) { try { - await cleanupExecutionWorkspaceArtifacts({ - workspace: { - id: - reusableExistingExecutionWorkspace?.id - ?? workspaceReuseRequest.requestedExecutionWorkspaceId - ?? `transient-${run.id}`, - cwd: executionWorkspace.cwd, - providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs", - providerRef: executionWorkspace.worktreePath, - branchName: executionWorkspace.branchName, - repoUrl: executionWorkspace.repoUrl, - baseRef: executionWorkspace.repoRef, - projectId: resolvedProjectId, - projectWorkspaceId: resolvedProjectWorkspaceId, - sourceIssueId: issueRef?.id ?? null, - metadata: nextExecutionWorkspaceMetadata, - }, - projectWorkspace: { - cwd: resolvedWorkspace.cwd, - cleanupCommand: null, - }, - cleanupCommand: configSnapshot?.cleanupCommand ?? null, - teardownCommand: configSnapshot?.teardownCommand ?? projectExecutionWorkspacePolicy?.workspaceStrategy?.teardownCommand ?? null, - recorder: workspaceOperationRecorder, - }); - } catch (cleanupError) { + persistedWorktreeInstanceRoot = + ( + await readManagedWorktreeInstanceOwnership( + executionWorkspace.worktreePath, + ) + )?.instanceRoot ?? null; + } catch (error) { logger.warn( { runId: run.id, issueId, executionWorkspaceCwd: executionWorkspace.cwd, - cleanupError: cleanupError instanceof Error ? cleanupError.message : String(cleanupError), + error: error instanceof Error ? error.message : String(error), }, - "Failed to cleanup realized execution workspace after persistence failure", + "Could not record managed worktree instance ownership", ); } } - throw error; - } - await workspaceOperationRecorder.attachExecutionWorkspaceId(persistedExecutionWorkspace?.id ?? null); - await recordWorkspaceConfigFreshnessOperation({ - recorder: workspaceOperationRecorder, - runId: run.id, - decision: workspaceConfigFreshness, - hasExistingWorkspace: Boolean(reusableExistingExecutionWorkspace), - reuseRequested: requestedShouldReuseExisting, - workspaceReused: Boolean(reusedExecutionWorkspace), - configSnapshotRefreshed: resolvedWorkspaceReusePolicy.shouldRefreshWorkspaceConfigSnapshot, - previousWorkspaceId: workspaceReuseRequest.requestedExecutionWorkspaceId, - activeWorkspaceId: persistedExecutionWorkspace?.id ?? null, - }); - if ( - reusableExistingExecutionWorkspace && - persistedExecutionWorkspace && - reusableExistingExecutionWorkspace.id !== persistedExecutionWorkspace.id && - reusableExistingExecutionWorkspace.status === "active" - ) { - await executionWorkspacesSvc.update(reusableExistingExecutionWorkspace.id, { - status: "idle", - cleanupReason: null, - }); - } - if (issueId && persistedExecutionWorkspace && !nativeRecoveryExecutionWorkspaceId) { - const nextIssueWorkspaceMode = issueExecutionWorkspaceModeForPersistedWorkspace(persistedExecutionWorkspace.mode); - const shouldSwitchIssueToExistingWorkspace = - issueRef?.executionWorkspacePreference === "reuse_existing" || - requestedExecutionWorkspaceMode === "isolated_workspace" || - requestedExecutionWorkspaceMode === "operator_branch"; - const nextIssuePatch: Record = {}; - if (issueRef?.executionWorkspaceId !== persistedExecutionWorkspace.id) { - nextIssuePatch.executionWorkspaceId = persistedExecutionWorkspace.id; - } - if (resolvedProjectWorkspaceId && issueRef?.projectWorkspaceId !== resolvedProjectWorkspaceId) { - nextIssuePatch.projectWorkspaceId = resolvedProjectWorkspaceId; - } - if (shouldSwitchIssueToExistingWorkspace) { - nextIssuePatch.executionWorkspacePreference = "reuse_existing"; - nextIssuePatch.executionWorkspaceSettings = { - ...(issueExecutionWorkspaceSettings ?? {}), - mode: nextIssueWorkspaceMode, - }; - } - if (Object.keys(nextIssuePatch).length > 0) { - await issuesSvc.update(issueId, nextIssuePatch); - } - } - if (persistedExecutionWorkspace) { - context.executionWorkspaceId = persistedExecutionWorkspace.id; - await db - .update(heartbeatRuns) - .set({ - contextSnapshot: context, - updatedAt: new Date(), - }) - .where(eq(heartbeatRuns.id, run.id)); - } - const acquiredEnvironment = await envOrchestrator.acquireForRun({ - companyId: agent.companyId, - selectedEnvironmentId, - localEnvironmentId: localEnvironment.id, - adapterType: agent.adapterType, - issueId: issueId ?? null, - heartbeatRunId: run.id, - agentId: agent.id, - persistedExecutionWorkspace, - executionWorkspaceSettings: environmentExecutionWorkspaceSettings, - }); - const selectedEnvironment = acquiredEnvironment.environment; - // Defense-in-depth: re-check the actually-acquired environment against the - // execution allowlist. Even if selection were bypassed, a denied (local/ssh/ - // non-k8s) environment FAILS the run here rather than executing untrusted. - const allowlistDecision = evaluateExecutionAllowlist(executionPolicy, { - driver: selectedEnvironment.driver, - provider: - typeof selectedEnvironment.config?.provider === "string" - ? selectedEnvironment.config.provider - : null, - }); - if (!allowlistDecision.allowed) { - logger.error( - { - runId: run.id, - issueId, - agentId: agent.id, - environmentId: selectedEnvironment.id, - deniedDriver: allowlistDecision.deniedDriver, - deniedProvider: allowlistDecision.deniedProvider, - }, - "Execution allowlist denied the resolved environment; failing run", - ); - throw new Error(allowlistDecision.reason); - } - let activeEnvironmentLease = { - environment: acquiredEnvironment.environment, - lease: acquiredEnvironment.lease, - leaseContext: acquiredEnvironment.leaseContext, - }; - const realizationResult = await envOrchestrator.realizeForRun({ - environment: selectedEnvironment, - lease: activeEnvironmentLease.lease, - adapterType: agent.adapterType, - companyId: agent.companyId, - issueId: issueId ?? null, - heartbeatRunId: run.id, - executionWorkspace, - effectiveExecutionWorkspaceMode, - persistedExecutionWorkspace, - }); - activeEnvironmentLease = { - ...activeEnvironmentLease, - lease: realizationResult.lease, - }; - persistedExecutionWorkspace = realizationResult.persistedExecutionWorkspace; - const workspaceRealization = realizationResult.workspaceRealization; - const executionTarget = realizationResult.executionTarget; - const remoteExecution = realizationResult.remoteExecution; - if (!executionTarget || executionTarget.kind === "local") { - try { - runScratch = await prepareHeartbeatRunScratch({ - companyId: agent.companyId, - agentId: agent.id, - runId: run.id, - issueId: issueRef?.id ?? null, - issueIdentifier: issueRef?.identifier ?? null, - }); - const existingRuntimeEnv = parseObject(runtimeConfig.env); - const scratchEnv = buildHeartbeatRunScratchEnv(existingRuntimeEnv, runScratch); - runtimeConfig = { - ...runtimeConfig, - env: { - ...existingRuntimeEnv, - ...scratchEnv.env, - }, - }; - context.paperclipScratch = { - type: "heartbeat_run", - dir: runScratch.dir, - cleanupPolicy: "terminal_run", - marker: HEARTBEAT_RUN_SCRATCH_MARKER, - tempKeysApplied: scratchEnv.tempKeysApplied, - }; - } catch (scratchPrepareError) { - runScratch = null; - delete context.paperclipScratch; - logger.warn( - { - err: scratchPrepareError, - runId: run.id, - issueId, - agentId: agent.id, - }, - "failed to prepare heartbeat run scratch directory; continuing without scratch env", - ); - } - } else { - delete context.paperclipScratch; - } - context.paperclipEnvironment = { - id: selectedEnvironment.id, - name: selectedEnvironment.name, - driver: selectedEnvironment.driver, - leaseId: activeEnvironmentLease.lease.id, - workspaceRealization, - ...(typeof activeEnvironmentLease.lease.metadata?.remoteCwd === "string" - ? { - remoteCwd: activeEnvironmentLease.lease.metadata.remoteCwd, - host: - typeof activeEnvironmentLease.lease.metadata?.host === "string" - ? activeEnvironmentLease.lease.metadata.host - : undefined, - port: - typeof activeEnvironmentLease.lease.metadata?.port === "number" - ? activeEnvironmentLease.lease.metadata.port - : undefined, - username: - typeof activeEnvironmentLease.lease.metadata?.username === "string" - ? activeEnvironmentLease.lease.metadata.username - : undefined, - } - : {}), - }; - await db - .update(heartbeatRuns) - .set({ - contextSnapshot: context, - updatedAt: new Date(), - }) - .where(eq(heartbeatRuns.id, run.id)); - const runtimeSessionResolution = resolveRuntimeSessionParamsForWorkspace({ - agentId: agent.id, - previousSessionParams, - resolvedWorkspace: { - ...resolvedWorkspace, - cwd: executionWorkspace.cwd, - }, - }); - const runtimeSessionParams = runtimeSessionResolution.sessionParams; - const runtimeWorkspaceWarnings = [ - ...resolvedWorkspace.warnings, - ...executionWorkspace.warnings, - ...(runtimeSessionResolution.warning ? [runtimeSessionResolution.warning] : []), - ...(requestedShouldReuseExisting && workspaceConfigFreshness.reasons.length > 0 - ? [ - `Execution workspace reuse freshness action "${workspaceConfigFreshness.action}" because ${workspaceConfigFreshness.reasons.join("; ")}.`, - ] - : []), - ...(resetTaskSession && sessionResetReason - ? [ - taskKey - ? `Skipping saved session resume for task "${taskKey}" because ${sessionResetReason}.` - : `Skipping saved session resume because ${sessionResetReason}.`, - ] - : []), - ]; - context.paperclipWorkspace = { - cwd: executionWorkspace.cwd, - source: executionWorkspace.source, - mode: effectiveExecutionWorkspaceMode, - strategy: executionWorkspace.strategy, - projectId: executionWorkspace.projectId, - workspaceId: executionWorkspace.workspaceId, - repoUrl: executionWorkspace.repoUrl, - repoRef: executionWorkspace.repoRef, - branchName: executionWorkspace.branchName, - worktreePath: executionWorkspace.worktreePath, - realization: workspaceRealization, - agentHome: await (async () => { - const home = resolveDefaultAgentWorkspaceDir(agent.id); - await fs.mkdir(home, { recursive: true }); - return home; - })(), - }; - context.paperclipWorkspaces = buildRunWorkspaceHints(resolvedWorkspace); - // Emit exactly one requested-vs-synced observability line for the referenced-project set. A run - // with no referenced project stays silent, so this adds no noise to the anchor-only default. The - // per-drop human warning already rides `runtimeWorkspaceWarnings`; this line carries the counts - // and the per-failure reason for a partial sync. - const referencedProjectObservability = buildReferencedProjectRunObservability({ - syncedProjectIds: resolvedWorkspace.additionalWorkspaces.map( - (additional) => additional.projectId, - ), - failures: resolvedWorkspace.referencedProjectFailures, - }); - if (referencedProjectObservability.referenced_projects_requested > 0) { - logger.info( - { - runId: run.id, - companyId: agent.companyId, - issueId: issueRef?.id ?? null, - ...referencedProjectObservability, - }, - "run referenced-project sync", - ); - } - // The wake payload is built before the execution workspace is resolved, so - // attach the branch pin here; the shared wake-prompt renderer surfaces it as - // a one-time "stay on this branch" hint on non-resumed sessions. - if (executionWorkspace.branchName) { - const wakePayloadForWorkspace = parseObject(context[PAPERCLIP_WAKE_PAYLOAD_KEY]); - context[PAPERCLIP_WAKE_PAYLOAD_KEY] = { - ...wakePayloadForWorkspace, - executionWorkspace: { branchName: executionWorkspace.branchName }, + const nextExecutionWorkspaceMetadata = { + ...baseExecutionWorkspaceMetadata, + ...(persistedWorktreeInstanceRoot + ? { + [WORKTREE_INSTANCE_ROOT_METADATA_KEY]: + persistedWorktreeInstanceRoot, + } + : {}), }; - } - const runtimeServiceIntents = (() => { - const runtimeConfig = parseObject(hostExecutionWorkspaceConfig.workspaceRuntime); - return Array.isArray(runtimeConfig.services) - ? runtimeConfig.services.filter( - (value): value is Record => typeof value === "object" && value !== null, - ) - : []; - })(); - assertLowTrustRuntimeServicesAllowed({ - resolution: trustPreset, - runtimeServiceCount: runtimeServiceIntents.length, - }); - if (runtimeServiceIntents.length > 0) { - context.paperclipRuntimeServiceIntents = runtimeServiceIntents; - } else { - delete context.paperclipRuntimeServiceIntents; - } - if (executionWorkspace.projectId && !readNonEmptyString(context.projectId)) { - context.projectId = executionWorkspace.projectId; - } - const runtimeSessionFallback = taskKey || resetTaskSession - ? null - : isCanonicalSessionIdForAdapter(agent.adapterType, runtime.sessionId) - ? runtime.sessionId - : null; - const runtimeSessionDisplayId = truncateDisplayId( - explicitResumeSessionDisplayId ?? - taskSessionForRun?.sessionDisplayId ?? - (sessionCodec.getDisplayId ? sessionCodec.getDisplayId(runtimeSessionParams) : null) ?? - readNonEmptyString(runtimeSessionParams?.sessionId) ?? - runtimeSessionFallback, - ); - let previousSessionDisplayId = requiresCanonicalSessionIds(agent.adapterType) - ? truncateDisplayId( - readNonEmptyString(previousSessionParams?.sessionId) ?? - (isCanonicalSessionIdForAdapter(agent.adapterType, runtimeSessionDisplayId) ? runtimeSessionDisplayId : null) ?? - runtimeSessionFallback, - ) - : runtimeSessionDisplayId; - let runtimeSessionIdForAdapter = - readNonEmptyString(runtimeSessionParams?.sessionId) ?? runtimeSessionFallback; - let runtimeSessionParamsForAdapter = normalizeSessionParams( - stripPaperclipSessionMetadataFromSessionParams(runtimeSessionParams), - ); - - const sessionCompaction = await evaluateSessionCompaction({ - agent, - sessionId: previousSessionDisplayId ?? runtimeSessionIdForAdapter, - issueId, - continuationSummaryBody: continuationSummary?.body ?? null, - }); - if (sessionCompaction.rotate) { - context.paperclipSessionHandoffMarkdown = sessionCompaction.handoffMarkdown; - context.paperclipSessionRotationReason = sessionCompaction.reason; - context.paperclipPreviousSessionId = previousSessionDisplayId ?? runtimeSessionIdForAdapter; - runtimeSessionIdForAdapter = null; - runtimeSessionParamsForAdapter = null; - previousSessionDisplayId = null; - if (sessionCompaction.reason) { - runtimeWorkspaceWarnings.push( - `Starting a fresh session because ${sessionCompaction.reason}.`, - ); + const pendingForwardBranchReconcile = + executionWorkspace.pendingForwardBranchReconcile ?? null; + const branchNameForInitialPersistence = + pendingForwardBranchReconcile?.recordedBranchName ?? + executionWorkspace.branchName; + try { + persistedExecutionWorkspace = + resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace && + reusableExistingExecutionWorkspace + ? await executionWorkspacesSvc.update( + reusableExistingExecutionWorkspace.id, + { + cwd: executionWorkspace.cwd, + repoUrl: executionWorkspace.repoUrl, + baseRef: executionWorkspace.repoRef, + branchName: branchNameForInitialPersistence, + providerType: + executionWorkspace.strategy === "git_worktree" + ? "git_worktree" + : "local_fs", + providerRef: executionWorkspace.worktreePath, + status: "active", + lastUsedAt: new Date(), + metadata: nextExecutionWorkspaceMetadata, + projectWorkspaceId: + reconcileReusedExecutionWorkspaceProjectWorkspaceId( + reusableExistingExecutionWorkspace.projectWorkspaceId, + resolvedProjectWorkspaceId, + ), + }, + ) + : resolvedProjectId + ? await executionWorkspacesSvc.create({ + companyId: agent.companyId, + projectId: resolvedProjectId, + projectWorkspaceId: resolvedProjectWorkspaceId, + sourceIssueId: issueRef?.id ?? null, + mode: + requestedExecutionWorkspaceMode === "isolated_workspace" + ? "isolated_workspace" + : requestedExecutionWorkspaceMode === "operator_branch" + ? "operator_branch" + : requestedExecutionWorkspaceMode === "agent_default" + ? "adapter_managed" + : "shared_workspace", + strategyType: + executionWorkspace.strategy === "git_worktree" + ? "git_worktree" + : "project_primary", + name: + branchNameForInitialPersistence ?? + issueRef?.identifier ?? + `workspace-${agent.id.slice(0, 8)}`, + status: "active", + cwd: executionWorkspace.cwd, + repoUrl: executionWorkspace.repoUrl, + baseRef: executionWorkspace.repoRef, + branchName: branchNameForInitialPersistence, + providerType: + executionWorkspace.strategy === "git_worktree" + ? "git_worktree" + : "local_fs", + providerRef: executionWorkspace.worktreePath, + lastUsedAt: new Date(), + openedAt: new Date(), + metadata: nextExecutionWorkspaceMetadata, + }) + : null; + } catch (error) { + if (executionWorkspace.created) { + try { + await cleanupExecutionWorkspaceArtifacts({ + workspace: { + id: + reusableExistingExecutionWorkspace?.id ?? + workspaceReuseRequest.requestedExecutionWorkspaceId ?? + `transient-${run.id}`, + cwd: executionWorkspace.cwd, + providerType: + executionWorkspace.strategy === "git_worktree" + ? "git_worktree" + : "local_fs", + providerRef: executionWorkspace.worktreePath, + branchName: executionWorkspace.branchName, + repoUrl: executionWorkspace.repoUrl, + baseRef: executionWorkspace.repoRef, + projectId: resolvedProjectId, + projectWorkspaceId: resolvedProjectWorkspaceId, + sourceIssueId: issueRef?.id ?? null, + metadata: nextExecutionWorkspaceMetadata, + }, + projectWorkspace: { + cwd: resolvedWorkspace.cwd, + cleanupCommand: null, + }, + cleanupCommand: configSnapshot?.cleanupCommand ?? null, + teardownCommand: + configSnapshot?.teardownCommand ?? + projectExecutionWorkspacePolicy?.workspaceStrategy + ?.teardownCommand ?? + null, + recorder: workspaceOperationRecorder, + }); + } catch (cleanupError) { + logger.warn( + { + runId: run.id, + issueId, + executionWorkspaceCwd: executionWorkspace.cwd, + cleanupError: + cleanupError instanceof Error + ? cleanupError.message + : String(cleanupError), + }, + "Failed to cleanup realized execution workspace after persistence failure", + ); + } + } + throw error; } - } else { - delete context.paperclipSessionHandoffMarkdown; - delete context.paperclipSessionRotationReason; - delete context.paperclipPreviousSessionId; - } - - const runtimeForAdapter = { - sessionId: runtimeSessionIdForAdapter, - sessionParams: runtimeSessionParamsForAdapter, - sessionDisplayId: previousSessionDisplayId, - taskKey, - }; - const configFreshnessResultMetadata = { - version: sessionConfigMetadata.version, - session: { - fingerprintVersion: sessionConfigMetadata.version, - categories: sessionConfigMetadata.categories, - reset: resetTaskSession, - resetReasons: sessionConfigFreshness.reasons, - changedCategories: sessionConfigFreshness.changedCategories, - taskSessionAvailable: taskSession != null, - taskSessionReused: taskSessionForRun != null, - storedFingerprintPresent: Boolean(sessionConfigFreshness.storedFingerprint), - nextFingerprint: sessionConfigFreshness.nextFingerprint, - }, - workspace: { - fingerprintVersion: latestWorkspaceConfigMetadata.version, - categories: latestWorkspaceConfigMetadata.categories, - action: workspaceConfigFreshness.action, - changedCategories: workspaceConfigFreshness.changedCategories, - reasons: workspaceConfigFreshness.reasons, + await workspaceOperationRecorder.attachExecutionWorkspaceId( + persistedExecutionWorkspace?.id ?? null, + ); + await recordWorkspaceConfigFreshnessOperation({ + recorder: workspaceOperationRecorder, + runId: run.id, + decision: workspaceConfigFreshness, + hasExistingWorkspace: Boolean(reusableExistingExecutionWorkspace), reuseRequested: requestedShouldReuseExisting, workspaceReused: Boolean(reusedExecutionWorkspace), - configSnapshotRefreshed: resolvedWorkspaceReusePolicy.shouldRefreshWorkspaceConfigSnapshot, - storedFingerprintPresent: workspaceConfigFreshness.storedFingerprintPresent, - storedFingerprint: workspaceConfigFreshness.storedFingerprint, - inferredFingerprint: workspaceConfigFreshness.inferredFingerprint, - nextFingerprint: workspaceConfigFreshness.nextFingerprint, - previousWorkspaceId: workspaceReuseRequest.requestedExecutionWorkspaceId, + configSnapshotRefreshed: + resolvedWorkspaceReusePolicy.shouldRefreshWorkspaceConfigSnapshot, + previousWorkspaceId: + workspaceReuseRequest.requestedExecutionWorkspaceId, activeWorkspaceId: persistedExecutionWorkspace?.id ?? null, - }, - }; - - let seq = 1; - let handle: RunLogHandle | null = null; - let stdoutExcerpt = ""; - let stderrExcerpt = ""; - let outputSeq = Number(run.lastOutputSeq ?? 0); - let lastOutputFlushAt: Date | null = run.lastOutputAt ?? null; - let lastLogRuntimeStatusTouchMs = 0; - const outputProgressState: { - pending: { - at: Date; - seq: number; - stream: "stdout" | "stderr"; - bytes: number; - } | null; - } = { pending: null }; - let persistedLogBytes = Number(run.logBytes ?? 0); - const flushOutputProgress = async (opts?: { force?: boolean }) => { - const pendingOutputProgress = outputProgressState.pending; - if (!pendingOutputProgress) return; - const shouldFlush = - opts?.force === true || - !lastOutputFlushAt || - pendingOutputProgress.at.getTime() - lastOutputFlushAt.getTime() >= ACTIVE_RUN_OUTPUT_PROGRESS_FLUSH_INTERVAL_MS; - if (!shouldFlush) return; - await db - .update(heartbeatRuns) - .set({ - lastOutputAt: pendingOutputProgress.at, - lastOutputSeq: pendingOutputProgress.seq, - lastOutputStream: pendingOutputProgress.stream, - lastOutputBytes: pendingOutputProgress.bytes, - updatedAt: new Date(), - }) - .where(eq(heartbeatRuns.id, run.id)); - lastOutputFlushAt = pendingOutputProgress.at; - outputProgressState.pending = null; - }; - try { - const startedAt = run.startedAt ?? new Date(); - const runningWithSession = await db - .update(heartbeatRuns) - .set({ - startedAt, - sessionIdBefore: runtimeForAdapter.sessionDisplayId ?? runtimeForAdapter.sessionId, - contextSnapshot: context, - updatedAt: new Date(), - }) - .where(eq(heartbeatRuns.id, run.id)) - .returning() - .then((rows) => rows[0] ?? null); - if (runningWithSession) run = runningWithSession; - - // Pause Durability: flip to "running" ONLY if the agent is still invokable. - // Atomic conditional UPDATE is the sole gate (no read-then-write); 0 rows => abort. - const runningAgent = await db - .update(agents) - .set({ status: "running", updatedAt: new Date() }) - .where(and(eq(agents.id, agent.id), notInArray(agents.status, [...DIRECT_NON_INVOKABLE_STATUSES]))) - .returning() - .then((rows) => rows[0] ?? null); - - if (!runningAgent) { - logger.warn( - { agentId: agent.id, runId: run.id, previousStatus: agent.status }, - "execution-start aborted: agent not invokable", - ); - const abortReason = "Cancelled: agent not invokable at execution-start"; - await setRunStatus(run.id, "cancelled", { - finishedAt: new Date(), - error: abortReason, - errorCode: "agent_not_invokable", - ...(agent ? { - resultJson: mergeRunStopMetadataForAgent(agent, "cancelled", { - resultJson: parseObject(run.resultJson), - errorCode: "agent_not_invokable", - errorMessage: abortReason, - }), - } : {}), - }); - await setWakeupStatus(run.wakeupRequestId, "cancelled", { - finishedAt: new Date(), - error: abortReason, - }); - await releaseIssueExecutionAndPromote(run); - return; - } - - publishLiveEvent({ - companyId: runningAgent.companyId, - type: "agent.status", - payload: { - agentId: runningAgent.id, - status: runningAgent.status, - outcome: "running", - }, }); - - const currentRun = run; - await appendRunEvent(currentRun, seq++, { - eventType: "lifecycle", - stream: "system", - level: "info", - message: "run started", - }); - - handle = await runLogStore.begin({ - companyId: run.companyId, - agentId: run.agentId, - runId, - }); - - await db - .update(heartbeatRuns) - .set({ - logStore: handle.store, - logRef: handle.logRef, - updatedAt: new Date(), - }) - .where(eq(heartbeatRuns.id, runId)); - - const currentUserRedactionOptions = await getCurrentUserRedactionOptions(); - const onLog = async (stream: "stdout" | "stderr", chunk: string) => { - const sanitizedChunk = compactRunLogChunk( - redactCurrentUserText(chunk, currentUserRedactionOptions), - ); - if (stream === "stdout") stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk); - if (stream === "stderr") stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk); - const ts = new Date().toISOString(); - - outputSeq += 1; - const chunkSeq = outputSeq; - let appendedBytes = 0; - if (handle) { - appendedBytes = await runLogStore.append(handle, { - stream, - chunk: sanitizedChunk, - ts, - seq: chunkSeq, - }); - persistedLogBytes += appendedBytes; - } - outputProgressState.pending = { - at: new Date(ts), - seq: chunkSeq, - stream, - bytes: persistedLogBytes, - }; - await flushOutputProgress(); - - // Streamed CLI output is real run activity: keep the in-memory - // runtime status ("Working... / X ago") fresh between structured - // events so sandbox runs with mid-run log streaming never show a - // minutes-stale timestamp. Throttled to avoid churning the live - // event stream on every 250ms tail chunk. - const logActivityAt = new Date(ts); - if ( - isHeartbeatRunRuntimeStatusActive(run.status) && - logActivityAt.getTime() - lastLogRuntimeStatusTouchMs >= - ACTIVE_RUN_LOG_RUNTIME_STATUS_REFRESH_INTERVAL_MS - ) { - lastLogRuntimeStatusTouchMs = logActivityAt.getTime(); - const touchedStatus = touchHeartbeatRunRuntimeStatus({ - companyId: run.companyId, - issueId, - agentId: run.agentId, - runId: run.id, - at: logActivityAt, - }); - if (touchedStatus) publishHeartbeatRunRuntimeProgress(touchedStatus); - } - - const payloadChunk = - sanitizedChunk.length > MAX_LIVE_LOG_CHUNK_BYTES - ? sanitizedChunk.slice(sanitizedChunk.length - MAX_LIVE_LOG_CHUNK_BYTES) - : sanitizedChunk; - - publishLiveEvent({ - companyId: run.companyId, - type: "heartbeat.run.log", - payload: { - runId: run.id, - agentId: run.agentId, - issueId, - ts, - seq: chunkSeq, - stream, - chunk: payloadChunk, - truncated: payloadChunk.length !== sanitizedChunk.length, + if ( + reusableExistingExecutionWorkspace && + persistedExecutionWorkspace && + reusableExistingExecutionWorkspace.id !== + persistedExecutionWorkspace.id && + reusableExistingExecutionWorkspace.status === "active" + ) { + await executionWorkspacesSvc.update( + reusableExistingExecutionWorkspace.id, + { + status: "idle", + cleanupReason: null, }, - }); - }; - if (runScopedMentionedSkillKeys.length > 0) { - await onLog( - "stdout", - `[paperclip] Enabled run-scoped skills from issue mentions: ${runScopedMentionedSkillKeys.join(", ")}\n`, ); } - for (const warning of runtimeWorkspaceWarnings) { - const logEntry = formatRuntimeWorkspaceWarningLog(warning); - await onLog(logEntry.stream, logEntry.chunk); + if ( + issueId && + persistedExecutionWorkspace && + !nativeRecoveryExecutionWorkspaceId + ) { + const nextIssueWorkspaceMode = + issueExecutionWorkspaceModeForPersistedWorkspace( + persistedExecutionWorkspace.mode, + ); + const shouldSwitchIssueToExistingWorkspace = + issueRef?.executionWorkspacePreference === "reuse_existing" || + requestedExecutionWorkspaceMode === "isolated_workspace" || + requestedExecutionWorkspaceMode === "operator_branch"; + const nextIssuePatch: Record = {}; + if (issueRef?.executionWorkspaceId !== persistedExecutionWorkspace.id) { + nextIssuePatch.executionWorkspaceId = persistedExecutionWorkspace.id; + } + if ( + resolvedProjectWorkspaceId && + issueRef?.projectWorkspaceId !== resolvedProjectWorkspaceId + ) { + nextIssuePatch.projectWorkspaceId = resolvedProjectWorkspaceId; + } + if (shouldSwitchIssueToExistingWorkspace) { + nextIssuePatch.executionWorkspacePreference = "reuse_existing"; + nextIssuePatch.executionWorkspaceSettings = { + ...(issueExecutionWorkspaceSettings ?? {}), + mode: nextIssueWorkspaceMode, + }; + } + if (Object.keys(nextIssuePatch).length > 0) { + await issuesSvc.update(issueId, nextIssuePatch); + } } - await assertGitSensitiveAdapterWorkspaceValid({ - adapterType: agent.adapterType, - agentId: agent.id, - issue: issueRef - ? { - id: issueRef.id, - identifier: issueRef.identifier, - projectId: issueRef.projectId, - projectWorkspaceId: issueRef.projectWorkspaceId, - } - : null, - resolvedWorkspace, - executionWorkspace, - persistedExecutionWorkspace, - executionTarget, - environmentDriver: selectedEnvironment.driver, - leaseMetadata: activeEnvironmentLease.lease.metadata, - }); - await assertPushCapabilityCheckoutValid({ - enabled: pushCapabilityPreflightRequired && executionTarget?.kind === "local", - issue: issueRef - ? { - id: issueRef.id, - identifier: issueRef.identifier, - } - : null, - cwd: executionWorkspace.cwd, - }); - const adapterEnv = Object.fromEntries( - Object.entries(parseObject(resolvedConfig.env)).filter( - (entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string", - ), - ); - const runtimeServices = await ensureRuntimeServicesForRun({ - db, - runId: run.id, - agent: { - id: agent.id, - name: agent.name, - companyId: agent.companyId, - }, - issue: issueRef, - workspace: executionWorkspace, - executionWorkspaceId: persistedExecutionWorkspace?.id ?? issueRef?.executionWorkspaceId ?? null, - config: hostExecutionWorkspaceConfig, - adapterEnv, - onLog, - recorder: workspaceOperationRecorder, - }); - if (runtimeServices.length > 0) { - context.paperclipRuntimeServices = runtimeServices; - context.paperclipRuntimePrimaryUrl = - runtimeServices.find((service) => readNonEmptyString(service.url))?.url ?? null; + if (persistedExecutionWorkspace) { + context.executionWorkspaceId = persistedExecutionWorkspace.id; await db .update(heartbeatRuns) .set({ @@ -15830,469 +17729,1375 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }) .where(eq(heartbeatRuns.id, run.id)); } - if (issueId && (executionWorkspace.created || runtimeServices.some((service) => !service.reused))) { - try { - await postWorkspaceReadyComment({ - issuesSvc, + const acquiredEnvironment = await envOrchestrator.acquireForRun({ + companyId: agent.companyId, + selectedEnvironmentId, + localEnvironmentId: localEnvironment.id, + adapterType: agent.adapterType, + issueId: issueId ?? null, + heartbeatRunId: run.id, + agentId: agent.id, + persistedExecutionWorkspace, + executionWorkspaceSettings: environmentExecutionWorkspaceSettings, + }); + const selectedEnvironment = acquiredEnvironment.environment; + // Defense-in-depth: re-check the actually-acquired environment against the + // execution allowlist. Even if selection were bypassed, a denied (local/ssh/ + // non-k8s) environment FAILS the run here rather than executing untrusted. + const allowlistDecision = evaluateExecutionAllowlist(executionPolicy, { + driver: selectedEnvironment.driver, + provider: + typeof selectedEnvironment.config?.provider === "string" + ? selectedEnvironment.config.provider + : null, + }); + if (!allowlistDecision.allowed) { + logger.error( + { + runId: run.id, issueId, agentId: agent.id, + environmentId: selectedEnvironment.id, + deniedDriver: allowlistDecision.deniedDriver, + deniedProvider: allowlistDecision.deniedProvider, + }, + "Execution allowlist denied the resolved environment; failing run", + ); + throw new Error(allowlistDecision.reason); + } + let activeEnvironmentLease = { + environment: acquiredEnvironment.environment, + lease: acquiredEnvironment.lease, + leaseContext: acquiredEnvironment.leaseContext, + }; + const realizationResult = await envOrchestrator.realizeForRun({ + environment: selectedEnvironment, + lease: activeEnvironmentLease.lease, + adapterType: agent.adapterType, + companyId: agent.companyId, + issueId: issueId ?? null, + heartbeatRunId: run.id, + executionWorkspace, + effectiveExecutionWorkspaceMode, + persistedExecutionWorkspace, + }); + activeEnvironmentLease = { + ...activeEnvironmentLease, + lease: realizationResult.lease, + }; + persistedExecutionWorkspace = + realizationResult.persistedExecutionWorkspace; + const workspaceRealization = realizationResult.workspaceRealization; + const executionTarget = realizationResult.executionTarget; + const remoteExecution = realizationResult.remoteExecution; + if (!executionTarget || executionTarget.kind === "local") { + try { + runScratch = await prepareHeartbeatRunScratch({ + companyId: agent.companyId, + agentId: agent.id, runId: run.id, - workspace: executionWorkspace, - runtimeServices, + issueId: issueRef?.id ?? null, + issueIdentifier: issueRef?.identifier ?? null, }); - } catch (err) { - await onLog( - "stderr", - `[paperclip] Failed to post workspace-ready comment: ${err instanceof Error ? err.message : String(err)}\n`, + const existingRuntimeEnv = parseObject(runtimeConfig.env); + const scratchEnv = buildHeartbeatRunScratchEnv( + existingRuntimeEnv, + runScratch, + ); + runtimeConfig = { + ...runtimeConfig, + env: { + ...existingRuntimeEnv, + ...scratchEnv.env, + }, + }; + context.paperclipScratch = { + type: "heartbeat_run", + dir: runScratch.dir, + cleanupPolicy: "terminal_run", + marker: HEARTBEAT_RUN_SCRATCH_MARKER, + tempKeysApplied: scratchEnv.tempKeysApplied, + }; + } catch (scratchPrepareError) { + runScratch = null; + delete context.paperclipScratch; + logger.warn( + { + err: scratchPrepareError, + runId: run.id, + issueId, + agentId: agent.id, + }, + "failed to prepare heartbeat run scratch directory; continuing without scratch env", ); } + } else { + delete context.paperclipScratch; } - const onAdapterMeta = async (meta: AdapterInvocationMeta) => { - if (meta.env && secretKeys.size > 0) { - for (const key of secretKeys) { - if (key in meta.env) meta.env[key] = "***REDACTED***"; - } + context.paperclipEnvironment = { + id: selectedEnvironment.id, + name: selectedEnvironment.name, + driver: selectedEnvironment.driver, + leaseId: activeEnvironmentLease.lease.id, + workspaceRealization, + ...(typeof activeEnvironmentLease.lease.metadata?.remoteCwd === "string" + ? { + remoteCwd: activeEnvironmentLease.lease.metadata.remoteCwd, + host: + typeof activeEnvironmentLease.lease.metadata?.host === "string" + ? activeEnvironmentLease.lease.metadata.host + : undefined, + port: + typeof activeEnvironmentLease.lease.metadata?.port === "number" + ? activeEnvironmentLease.lease.metadata.port + : undefined, + username: + typeof activeEnvironmentLease.lease.metadata?.username === + "string" + ? activeEnvironmentLease.lease.metadata.username + : undefined, + } + : {}), + }; + await db + .update(heartbeatRuns) + .set({ + contextSnapshot: context, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, run.id)); + const runtimeSessionResolution = resolveRuntimeSessionParamsForWorkspace({ + agentId: agent.id, + previousSessionParams, + resolvedWorkspace: { + ...resolvedWorkspace, + cwd: executionWorkspace.cwd, + }, + }); + const runtimeSessionParams = runtimeSessionResolution.sessionParams; + const runtimeWorkspaceWarnings = [ + ...resolvedWorkspace.warnings, + ...executionWorkspace.warnings, + ...(runtimeSessionResolution.warning + ? [runtimeSessionResolution.warning] + : []), + ...(requestedShouldReuseExisting && + workspaceConfigFreshness.reasons.length > 0 + ? [ + `Execution workspace reuse freshness action "${workspaceConfigFreshness.action}" because ${workspaceConfigFreshness.reasons.join("; ")}.`, + ] + : []), + ...(resetTaskSession && sessionResetReason + ? [ + taskKey + ? `Skipping saved session resume for task "${taskKey}" because ${sessionResetReason}.` + : `Skipping saved session resume because ${sessionResetReason}.`, + ] + : []), + ]; + context.paperclipWorkspace = { + cwd: executionWorkspace.cwd, + source: executionWorkspace.source, + mode: effectiveExecutionWorkspaceMode, + strategy: executionWorkspace.strategy, + projectId: executionWorkspace.projectId, + workspaceId: executionWorkspace.workspaceId, + repoUrl: executionWorkspace.repoUrl, + repoRef: executionWorkspace.repoRef, + branchName: executionWorkspace.branchName, + worktreePath: executionWorkspace.worktreePath, + realization: workspaceRealization, + agentHome: await (async () => { + const home = resolveDefaultAgentWorkspaceDir(agent.id); + await fs.mkdir(home, { recursive: true }); + return home; + })(), + }; + context.paperclipWorkspaces = buildRunWorkspaceHints(resolvedWorkspace); + // Emit exactly one requested-vs-synced observability line for the referenced-project set. A run + // with no referenced project stays silent, so this adds no noise to the anchor-only default. The + // per-drop human warning already rides `runtimeWorkspaceWarnings`; this line carries the counts + // and the per-failure reason for a partial sync. + const referencedProjectObservability = + buildReferencedProjectRunObservability({ + syncedProjectIds: resolvedWorkspace.additionalWorkspaces.map( + (additional) => additional.projectId, + ), + failures: resolvedWorkspace.referencedProjectFailures, + }); + if (referencedProjectObservability.referenced_projects_requested > 0) { + logger.info( + { + runId: run.id, + companyId: agent.companyId, + issueId: issueRef?.id ?? null, + ...referencedProjectObservability, + }, + "run referenced-project sync", + ); + } + // The wake payload is built before the execution workspace is resolved, so + // attach the branch pin here; the shared wake-prompt renderer surfaces it as + // a one-time "stay on this branch" hint on non-resumed sessions. + if (executionWorkspace.branchName) { + const wakePayloadForWorkspace = parseObject( + context[PAPERCLIP_WAKE_PAYLOAD_KEY], + ); + context[PAPERCLIP_WAKE_PAYLOAD_KEY] = { + ...wakePayloadForWorkspace, + executionWorkspace: { branchName: executionWorkspace.branchName }, + }; + } + const runtimeServiceIntents = (() => { + const runtimeConfig = parseObject( + hostExecutionWorkspaceConfig.workspaceRuntime, + ); + return Array.isArray(runtimeConfig.services) + ? runtimeConfig.services.filter( + (value): value is Record => + typeof value === "object" && value !== null, + ) + : []; + })(); + assertLowTrustRuntimeServicesAllowed({ + resolution: trustPreset, + runtimeServiceCount: runtimeServiceIntents.length, + }); + if (runtimeServiceIntents.length > 0) { + context.paperclipRuntimeServiceIntents = runtimeServiceIntents; + } else { + delete context.paperclipRuntimeServiceIntents; + } + if ( + executionWorkspace.projectId && + !readNonEmptyString(context.projectId) + ) { + context.projectId = executionWorkspace.projectId; + } + const runtimeSessionFallback = + taskKey || resetTaskSession + ? null + : isCanonicalSessionIdForAdapter(agent.adapterType, runtime.sessionId) + ? runtime.sessionId + : null; + const runtimeSessionDisplayId = truncateDisplayId( + explicitResumeSessionDisplayId ?? + taskSessionForRun?.sessionDisplayId ?? + (sessionCodec.getDisplayId + ? sessionCodec.getDisplayId(runtimeSessionParams) + : null) ?? + readNonEmptyString(runtimeSessionParams?.sessionId) ?? + runtimeSessionFallback, + ); + let previousSessionDisplayId = requiresCanonicalSessionIds( + agent.adapterType, + ) + ? truncateDisplayId( + readNonEmptyString(previousSessionParams?.sessionId) ?? + (isCanonicalSessionIdForAdapter( + agent.adapterType, + runtimeSessionDisplayId, + ) + ? runtimeSessionDisplayId + : null) ?? + runtimeSessionFallback, + ) + : runtimeSessionDisplayId; + let runtimeSessionIdForAdapter = + readNonEmptyString(runtimeSessionParams?.sessionId) ?? + runtimeSessionFallback; + let runtimeSessionParamsForAdapter = normalizeSessionParams( + stripPaperclipSessionMetadataFromSessionParams(runtimeSessionParams), + ); + + const sessionCompaction = await evaluateSessionCompaction({ + agent, + sessionId: previousSessionDisplayId ?? runtimeSessionIdForAdapter, + issueId, + continuationSummaryBody: continuationSummary?.body ?? null, + }); + if (sessionCompaction.rotate) { + context.paperclipSessionHandoffMarkdown = + sessionCompaction.handoffMarkdown; + context.paperclipSessionRotationReason = sessionCompaction.reason; + context.paperclipPreviousSessionId = + previousSessionDisplayId ?? runtimeSessionIdForAdapter; + runtimeSessionIdForAdapter = null; + runtimeSessionParamsForAdapter = null; + previousSessionDisplayId = null; + if (sessionCompaction.reason) { + runtimeWorkspaceWarnings.push( + `Starting a fresh session because ${sessionCompaction.reason}.`, + ); } - const modelProfileMetadata = modelProfileRunMetadata(modelProfileApplication); - await appendRunEvent(currentRun, seq++, { - eventType: "adapter.invoke", - stream: "system", - level: "info", - message: "adapter invocation", + } else { + delete context.paperclipSessionHandoffMarkdown; + delete context.paperclipSessionRotationReason; + delete context.paperclipPreviousSessionId; + } + + const runtimeForAdapter = { + sessionId: runtimeSessionIdForAdapter, + sessionParams: runtimeSessionParamsForAdapter, + sessionDisplayId: previousSessionDisplayId, + taskKey, + }; + const configFreshnessResultMetadata = { + version: sessionConfigMetadata.version, + session: { + fingerprintVersion: sessionConfigMetadata.version, + categories: sessionConfigMetadata.categories, + reset: resetTaskSession, + resetReasons: sessionConfigFreshness.reasons, + changedCategories: sessionConfigFreshness.changedCategories, + taskSessionAvailable: taskSession != null, + taskSessionReused: taskSessionForRun != null, + storedFingerprintPresent: Boolean( + sessionConfigFreshness.storedFingerprint, + ), + nextFingerprint: sessionConfigFreshness.nextFingerprint, + }, + workspace: { + fingerprintVersion: latestWorkspaceConfigMetadata.version, + categories: latestWorkspaceConfigMetadata.categories, + action: workspaceConfigFreshness.action, + changedCategories: workspaceConfigFreshness.changedCategories, + reasons: workspaceConfigFreshness.reasons, + reuseRequested: requestedShouldReuseExisting, + workspaceReused: Boolean(reusedExecutionWorkspace), + configSnapshotRefreshed: + resolvedWorkspaceReusePolicy.shouldRefreshWorkspaceConfigSnapshot, + storedFingerprintPresent: + workspaceConfigFreshness.storedFingerprintPresent, + storedFingerprint: workspaceConfigFreshness.storedFingerprint, + inferredFingerprint: workspaceConfigFreshness.inferredFingerprint, + nextFingerprint: workspaceConfigFreshness.nextFingerprint, + previousWorkspaceId: + workspaceReuseRequest.requestedExecutionWorkspaceId, + activeWorkspaceId: persistedExecutionWorkspace?.id ?? null, + }, + }; + + let seq = 1; + let handle: RunLogHandle | null = null; + let stdoutExcerpt = ""; + let stderrExcerpt = ""; + let outputSeq = Number(run.lastOutputSeq ?? 0); + let lastOutputFlushAt: Date | null = run.lastOutputAt ?? null; + let lastLogRuntimeStatusTouchMs = 0; + const outputProgressState: { + pending: { + at: Date; + seq: number; + stream: "stdout" | "stderr"; + bytes: number; + } | null; + } = { pending: null }; + let persistedLogBytes = Number(run.logBytes ?? 0); + const flushOutputProgress = async (opts?: { force?: boolean }) => { + const pendingOutputProgress = outputProgressState.pending; + if (!pendingOutputProgress) return; + const shouldFlush = + opts?.force === true || + !lastOutputFlushAt || + pendingOutputProgress.at.getTime() - lastOutputFlushAt.getTime() >= + ACTIVE_RUN_OUTPUT_PROGRESS_FLUSH_INTERVAL_MS; + if (!shouldFlush) return; + await db + .update(heartbeatRuns) + .set({ + lastOutputAt: pendingOutputProgress.at, + lastOutputSeq: pendingOutputProgress.seq, + lastOutputStream: pendingOutputProgress.stream, + lastOutputBytes: pendingOutputProgress.bytes, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, run.id)); + lastOutputFlushAt = pendingOutputProgress.at; + outputProgressState.pending = null; + }; + try { + const startedAt = run.startedAt ?? new Date(); + const runningWithSession = await db + .update(heartbeatRuns) + .set({ + startedAt, + sessionIdBefore: + runtimeForAdapter.sessionDisplayId ?? runtimeForAdapter.sessionId, + contextSnapshot: context, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, run.id)) + .returning() + .then((rows) => rows[0] ?? null); + if (runningWithSession) run = runningWithSession; + + // Pause Durability: flip to "running" ONLY if the agent is still invokable. + // Atomic conditional UPDATE is the sole gate (no read-then-write); 0 rows => abort. + const runningAgent = await db + .update(agents) + .set({ status: "running", updatedAt: new Date() }) + .where( + and( + eq(agents.id, agent.id), + notInArray(agents.status, [...DIRECT_NON_INVOKABLE_STATUSES]), + ), + ) + .returning() + .then((rows) => rows[0] ?? null); + + if (!runningAgent) { + logger.warn( + { agentId: agent.id, runId: run.id, previousStatus: agent.status }, + "execution-start aborted: agent not invokable", + ); + const abortReason = + "Cancelled: agent not invokable at execution-start"; + await setRunStatus(run.id, "cancelled", { + finishedAt: new Date(), + error: abortReason, + errorCode: "agent_not_invokable", + ...(agent + ? { + resultJson: mergeRunStopMetadataForAgent(agent, "cancelled", { + resultJson: parseObject(run.resultJson), + errorCode: "agent_not_invokable", + errorMessage: abortReason, + }), + } + : {}), + }); + await setWakeupStatus(run.wakeupRequestId, "cancelled", { + finishedAt: new Date(), + error: abortReason, + }); + await releaseIssueExecutionAndPromote(run); + return; + } + + publishLiveEvent({ + companyId: runningAgent.companyId, + type: "agent.status", payload: { - ...(meta as unknown as Record), - ...(modelProfileMetadata ? { modelProfile: modelProfileMetadata } : {}), + agentId: runningAgent.id, + status: runningAgent.status, + outcome: "running", }, }); - }; - const onAdapterEvent = async (event: AdapterRuntimeEvent) => { - const eventType = event.eventType.trim(); - if (!eventType) return; + const currentRun = run; await appendRunEvent(currentRun, seq++, { - eventType: eventType.slice(0, 120), - stream: event.stream, - level: event.level, - color: event.color, - message: event.message, - payload: event.payload, + eventType: "lifecycle", + stream: "system", + level: "info", + message: "run started", }); - }; - const adapter = getServerAdapter(agent.adapterType); - // Runtime selection is immutable once persisted. In particular, turning the instance flag - // off prevents new native runs without changing the recovery path for an already-native run. - const nativeRuntimeResolution = resolveHeartbeatNativeRuntimeMode({ - persisted: run, - enabled: resolvedInstanceSettings.experimental.enableNativeRunner === true, - runtimeConfig: agent.runtimeConfig, - adapterConfig: agent.adapterConfig, - agent: { id: agent.id, status: runningAgent.status, adapterType: agent.adapterType }, - issue: issueRef, - target: executionTarget, - workspaceId: persistedExecutionWorkspace?.id ?? null, - }); - let nativeExecution: NativeExecutionInput | null = null; - let nativeRunnerInstanceId: string | null = null; - if (nativeRuntimeResolution.kind === "native") { - if (!issueRef) { - throw new Error("native_runtime_ineligible: issue is required"); - } - const nativeExecutionWorkspaceId = persistedExecutionWorkspace?.id ?? run.id; - const persistedContract = run.completionContractId - ? await db.select().from(completionContracts) - .where(and( - eq(completionContracts.id, run.completionContractId), - eq(completionContracts.companyId, agent.companyId), - eq(completionContracts.issueId, issueRef.id), - )) - .limit(1) - .then((rows) => rows[0] ?? null) - : null; - const completionContract = persistedContract - ? { row: persistedContract, contract: persistedContract.contractJson as never } - : await ensureNativeCompletionContract({ - db, - companyId: agent.companyId, - issue: issueRef, - actorId: agent.id, - immediateRequest: safeWakeCommentContext?.body ?? null, + handle = await runLogStore.begin({ + companyId: run.companyId, + agentId: run.agentId, + runId, + }); + + await db + .update(heartbeatRuns) + .set({ + logStore: handle.store, + logRef: handle.logRef, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, runId)); + + const currentUserRedactionOptions = + await getCurrentUserRedactionOptions(); + const onLog = async (stream: "stdout" | "stderr", chunk: string) => { + const sanitizedChunk = compactRunLogChunk( + redactCurrentUserText(chunk, currentUserRedactionOptions), + ); + if (stream === "stdout") + stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk); + if (stream === "stderr") + stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk); + const ts = new Date().toISOString(); + + outputSeq += 1; + const chunkSeq = outputSeq; + let appendedBytes = 0; + if (handle) { + appendedBytes = await runLogStore.append(handle, { + stream, + chunk: sanitizedChunk, + ts, + seq: chunkSeq, }); - const taskNativeSessionId = readNonEmptyString(taskSessionDecodedParams?.sessionId); - const resumableTaskSessionId = - taskSessionForRun?.lastRunId - && taskSessionForRun.lastRunId !== run.id - && isNativeSessionId(taskNativeSessionId) - ? taskNativeSessionId - : null; - const previousNativeRun = resumableTaskSessionId && taskSessionForRun?.lastRunId - ? await db.select({ - id: heartbeatRuns.id, - companyId: heartbeatRuns.companyId, - agentId: heartbeatRuns.agentId, - runnerInstanceId: heartbeatRuns.runnerInstanceId, - nativeSessionId: heartbeatRuns.nativeSessionId, - runnerProfileJson: heartbeatRuns.runnerProfileJson, - }).from(heartbeatRuns).where(and( - eq(heartbeatRuns.id, taskSessionForRun.lastRunId), - eq(heartbeatRuns.companyId, agent.companyId), - eq(heartbeatRuns.agentId, agent.id), - eq(heartbeatRuns.nativeSessionId, resumableTaskSessionId), - )).limit(1).then((rows) => rows[0] ?? null) - : null; - nativeRunnerInstanceId = - previousNativeRun?.runnerInstanceId - && previousNativeRun.nativeSessionId === (run.nativeSessionId ?? resumableTaskSessionId) - ? previousNativeRun.runnerInstanceId - : run.runnerInstanceId ?? randomUUID(); - let nativeSessionId = run.nativeSessionId ?? resumableTaskSessionId ?? randomUUID(); - let nativeResumeCheckpoint: ReturnType = null; - const persistedProfile = persistedRunnerProfile; - if (persistedNativeExecutionInput) { - nativeExecution = persistedNativeExecutionInput; + persistedLogBytes += appendedBytes; + } + outputProgressState.pending = { + at: new Date(ts), + seq: chunkSeq, + stream, + bytes: persistedLogBytes, + }; + await flushOutputProgress(); + + // Streamed CLI output is real run activity: keep the in-memory + // runtime status ("Working... / X ago") fresh between structured + // events so sandbox runs with mid-run log streaming never show a + // minutes-stale timestamp. Throttled to avoid churning the live + // event stream on every 250ms tail chunk. + const logActivityAt = new Date(ts); if ( - nativeExecution.binding.companyId !== agent.companyId - || nativeExecution.binding.runId !== run.id - || nativeExecution.binding.issueId !== issueRef.id - || nativeExecution.binding.agentId !== agent.id - || nativeExecution.binding.executionWorkspaceId !== nativeExecutionWorkspaceId - || nativeExecution.completionContract.id !== completionContract.row.id - || nativeExecution.completionContract.sha256 !== completionContract.row.canonicalSha256 - ) throw new Error("native_execution_input_persisted_binding_mismatch"); - } else { - const interactionId = readNonEmptyString(context.interactionId); - const interactionResponses = await materializeNativeInteractionResponses({ - db, - companyId: agent.companyId, - issueId: issueRef.id, - runId: run.id, - agentId: agent.id, - interactionIds: interactionId ? [interactionId] : [], + isHeartbeatRunRuntimeStatusActive(run.status) && + logActivityAt.getTime() - lastLogRuntimeStatusTouchMs >= + ACTIVE_RUN_LOG_RUNTIME_STATUS_REFRESH_INTERVAL_MS + ) { + lastLogRuntimeStatusTouchMs = logActivityAt.getTime(); + const touchedStatus = touchHeartbeatRunRuntimeStatus({ + companyId: run.companyId, + issueId, + agentId: run.agentId, + runId: run.id, + at: logActivityAt, + }); + if (touchedStatus) + publishHeartbeatRunRuntimeProgress(touchedStatus); + } + + const payloadChunk = + sanitizedChunk.length > MAX_LIVE_LOG_CHUNK_BYTES + ? sanitizedChunk.slice( + sanitizedChunk.length - MAX_LIVE_LOG_CHUNK_BYTES, + ) + : sanitizedChunk; + + publishLiveEvent({ + companyId: run.companyId, + type: "heartbeat.run.log", + payload: { + runId: run.id, + agentId: run.agentId, + issueId, + ts, + seq: chunkSeq, + stream, + chunk: payloadChunk, + truncated: payloadChunk.length !== sanitizedChunk.length, + }, }); - const managedProfile = nativeRuntimeResolution.profile.backend === "claude_managed_agents_api" - ? await managedAgentProfileService(db).requireQualified( - agent.companyId, - readNonEmptyString(parseObject(runtimeConfig).managedProfileId) ?? "", - ) - : null; - const agentCoreProfile = nativeRuntimeResolution.profile.backend === "aws_agentcore_harness_api" - ? await remoteAgentProfileService(db).requireQualified( - agent.companyId, - readNonEmptyString(parseObject(agent.adapterConfig).agentCoreProfileId) ?? "", - "aws_bedrock_agentcore_harness", - ) - : null; - const agentCoreConfig = parseObject(agentCoreProfile?.configuration); - if (managedProfile) { - const rawApiKeyBinding = parseObject(parseObject(agent.adapterConfig).env).ANTHROPIC_API_KEY; - const boundSecretId = typeof rawApiKeyBinding === "object" && rawApiKeyBinding !== null - ? readNonEmptyString((rawApiKeyBinding as Record).secretId) - : null; - if (boundSecretId !== managedProfile.apiKeySecretId) { - throw new ConfigurationIncompleteFailure( - "configuration incomplete: Claude Agent profile API key is not bound at env.ANTHROPIC_API_KEY", - { - configurationIncomplete: { - reason: "managed_agent_profile_secret_binding_mismatch", - companyId: agent.companyId, - agentId: agent.id, - profileId: managedProfile.id, - requiredEnvKeys: ["ANTHROPIC_API_KEY"], - }, - }, - ); + }; + if (runScopedMentionedSkillKeys.length > 0) { + await onLog( + "stdout", + `[paperclip] Enabled run-scoped skills from issue mentions: ${runScopedMentionedSkillKeys.join(", ")}\n`, + ); + } + for (const warning of runtimeWorkspaceWarnings) { + const logEntry = formatRuntimeWorkspaceWarningLog(warning); + await onLog(logEntry.stream, logEntry.chunk); + } + await assertGitSensitiveAdapterWorkspaceValid({ + adapterType: agent.adapterType, + agentId: agent.id, + issue: issueRef + ? { + id: issueRef.id, + identifier: issueRef.identifier, + projectId: issueRef.projectId, + projectWorkspaceId: issueRef.projectWorkspaceId, + } + : null, + resolvedWorkspace, + executionWorkspace, + persistedExecutionWorkspace, + executionTarget, + environmentDriver: selectedEnvironment.driver, + leaseMetadata: activeEnvironmentLease.lease.metadata, + }); + await assertPushCapabilityCheckoutValid({ + enabled: + pushCapabilityPreflightRequired && + executionTarget?.kind === "local", + issue: issueRef + ? { + id: issueRef.id, + identifier: issueRef.identifier, + } + : null, + cwd: executionWorkspace.cwd, + }); + const adapterEnv = Object.fromEntries( + Object.entries(parseObject(resolvedConfig.env)).filter( + (entry): entry is [string, string] => + typeof entry[0] === "string" && typeof entry[1] === "string", + ), + ); + const runtimeServices = await ensureRuntimeServicesForRun({ + db, + runId: run.id, + agent: { + id: agent.id, + name: agent.name, + companyId: agent.companyId, + }, + issue: issueRef, + workspace: executionWorkspace, + executionWorkspaceId: + persistedExecutionWorkspace?.id ?? + issueRef?.executionWorkspaceId ?? + null, + config: hostExecutionWorkspaceConfig, + adapterEnv, + onLog, + recorder: workspaceOperationRecorder, + }); + if (runtimeServices.length > 0) { + context.paperclipRuntimeServices = runtimeServices; + context.paperclipRuntimePrimaryUrl = + runtimeServices.find((service) => readNonEmptyString(service.url)) + ?.url ?? null; + await db + .update(heartbeatRuns) + .set({ + contextSnapshot: context, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, run.id)); + } + if ( + issueId && + (executionWorkspace.created || + runtimeServices.some((service) => !service.reused)) + ) { + try { + await postWorkspaceReadyComment({ + issuesSvc, + issueId, + agentId: agent.id, + runId: run.id, + workspace: executionWorkspace, + runtimeServices, + }); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to post workspace-ready comment: ${err instanceof Error ? err.message : String(err)}\n`, + ); + } + } + const onAdapterMeta = async (meta: AdapterInvocationMeta) => { + if (meta.env && secretKeys.size > 0) { + for (const key of secretKeys) { + if (key in meta.env) meta.env[key] = "***REDACTED***"; } } - const executionMode = issueRef.workMode === "planning" && !acceptedPlanContinuationWake - ? "plan" as const - : "default" as const; - const pinnedPlan = executionMode === "plan" - ? await documentService(db).getIssueDocumentByKey(issueRef.id, "plan") + const modelProfileMetadata = modelProfileRunMetadata( + modelProfileApplication, + ); + await appendRunEvent(currentRun, seq++, { + eventType: "adapter.invoke", + stream: "system", + level: "info", + message: "adapter invocation", + payload: { + ...(meta as unknown as Record), + ...(modelProfileMetadata + ? { modelProfile: modelProfileMetadata } + : {}), + }, + }); + }; + + const onAdapterEvent = async (event: AdapterRuntimeEvent) => { + const eventType = event.eventType.trim(); + if (!eventType) return; + await appendRunEvent(currentRun, seq++, { + eventType: eventType.slice(0, 120), + stream: event.stream, + level: event.level, + color: event.color, + message: event.message, + payload: event.payload, + }); + }; + + const adapter = getServerAdapter(agent.adapterType); + // Runtime selection is immutable once persisted. In particular, turning the instance flag + // off prevents new native runs without changing the recovery path for an already-native run. + const nativeRuntimeResolution = resolveHeartbeatNativeRuntimeMode({ + persisted: run, + enabled: + resolvedInstanceSettings.experimental.enableNativeRunner === true, + runtimeConfig: agent.runtimeConfig, + adapterConfig: agent.adapterConfig, + agent: { + id: agent.id, + status: runningAgent.status, + adapterType: agent.adapterType, + }, + issue: issueRef, + target: executionTarget, + workspaceId: persistedExecutionWorkspace?.id ?? null, + }); + let nativeExecution: NativeExecutionInput | null = null; + let nativeRunnerInstanceId: string | null = null; + if (nativeRuntimeResolution.kind === "native") { + if (!issueRef) { + throw new Error("native_runtime_ineligible: issue is required"); + } + const nativeExecutionWorkspaceId = + persistedExecutionWorkspace?.id ?? run.id; + const persistedContract = run.completionContractId + ? await db + .select() + .from(completionContracts) + .where( + and( + eq(completionContracts.id, run.completionContractId), + eq(completionContracts.companyId, agent.companyId), + eq(completionContracts.issueId, issueRef.id), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) : null; - const pinnedReviewContext = executionMode === "plan" - ? await buildPlanReviewContext({ + const completionContract = persistedContract + ? { + row: persistedContract, + contract: persistedContract.contractJson as never, + } + : await ensureNativeCompletionContract({ + db, + companyId: agent.companyId, + issue: issueRef, + actorId: agent.id, + immediateRequest: safeWakeCommentContext?.body ?? null, + }); + const taskNativeSessionId = readNonEmptyString( + taskSessionDecodedParams?.sessionId, + ); + const resumableTaskSessionId = + taskSessionForRun?.lastRunId && + taskSessionForRun.lastRunId !== run.id && + isNativeSessionId(taskNativeSessionId) + ? taskNativeSessionId + : null; + const previousNativeRun = + resumableTaskSessionId && taskSessionForRun?.lastRunId + ? await db + .select({ + id: heartbeatRuns.id, + companyId: heartbeatRuns.companyId, + agentId: heartbeatRuns.agentId, + runnerInstanceId: heartbeatRuns.runnerInstanceId, + nativeSessionId: heartbeatRuns.nativeSessionId, + runnerProfileJson: heartbeatRuns.runnerProfileJson, + }) + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.id, taskSessionForRun.lastRunId), + eq(heartbeatRuns.companyId, agent.companyId), + eq(heartbeatRuns.agentId, agent.id), + eq(heartbeatRuns.nativeSessionId, resumableTaskSessionId), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) + : null; + nativeRunnerInstanceId = + previousNativeRun?.runnerInstanceId && + previousNativeRun.nativeSessionId === + (run.nativeSessionId ?? resumableTaskSessionId) + ? previousNativeRun.runnerInstanceId + : (run.runnerInstanceId ?? randomUUID()); + let nativeSessionId = + run.nativeSessionId ?? resumableTaskSessionId ?? randomUUID(); + let nativeResumeCheckpoint: ReturnType< + typeof rebindNativeSessionCheckpoint + > = null; + const persistedProfile = persistedRunnerProfile; + if (persistedNativeExecutionInput) { + nativeExecution = persistedNativeExecutionInput; + if ( + nativeExecution.binding.companyId !== agent.companyId || + nativeExecution.binding.runId !== run.id || + nativeExecution.binding.issueId !== issueRef.id || + nativeExecution.binding.agentId !== agent.id || + nativeExecution.binding.executionWorkspaceId !== + nativeExecutionWorkspaceId || + nativeExecution.completionContract.id !== + completionContract.row.id || + nativeExecution.completionContract.sha256 !== + completionContract.row.canonicalSha256 + ) + throw new Error( + "native_execution_input_persisted_binding_mismatch", + ); + } else { + const interactionId = readNonEmptyString(context.interactionId); + const interactionResponses = + await materializeNativeInteractionResponses({ db, companyId: agent.companyId, issueId: issueRef.id, - issueWorkMode: issueRef.workMode, - interactionId: readNonEmptyString(context.interactionId), - }) - : null; - const pinnedPlanMarkdown = pinnedPlan?.body ?? ""; - nativeExecution = buildNativeExecutionInput({ - companyId: agent.companyId, - runId: run.id, - issue: issueRef, - taskPrompt: readNonEmptyString(context.paperclipTaskMarkdown) - ?? `# ${issueRef.identifier ?? issueRef.id}: ${issueRef.title}`, - agentId: agent.id, - workspace: { - // Projectless paperclip_runner tasks still have a resolved local cwd. Bind that - // transient workspace to the run id so the native input remains durable and replayable - // without fabricating a project-scoped execution_workspaces row. - id: nativeExecutionWorkspaceId, - cwd: executionWorkspace.cwd, - repoUrl: executionWorkspace.repoUrl, - repoRef: executionWorkspace.repoRef, - branchName: executionWorkspace.branchName, - }, - normalizedSessionId: nativeSessionId, - executionMode, - planningContext: executionMode === "plan" ? { - documentId: pinnedPlan?.id ?? null, - baseRevisionId: pinnedPlan?.latestRevisionId ?? null, - baseRevisionNumber: pinnedPlan?.latestRevisionNumber ?? 0, - markdown: pinnedPlanMarkdown, - sha256: createHash("sha256").update(pinnedPlanMarkdown).digest("hex"), - reviewContext: pinnedReviewContext - ? structuredClone(pinnedReviewContext) as unknown as Record - : {}, - } : null, - provider: nativeRuntimeResolution.profile.backend === "opencode_server" - ? "opencode" - : nativeRuntimeResolution.profile.backend === "claude_managed_agents_api" - ? "claude_managed" - : nativeRuntimeResolution.profile.backend === "aws_agentcore_harness_api" - ? "aws_agentcore" - : nativeRuntimeResolution.profile.backend === "acpx_runtime" - ? "acpx" - : "codex", - ...(nativeRuntimeResolution.profile.backend === "acpx_runtime" ? { - acpxAgent: parseObject(runtimeConfig).acpxAgent as "pi" | "claude" | "codex", - } : {}), - model: typeof parseObject(agent.adapterConfig).model === "string" - ? String(parseObject(agent.adapterConfig).model) - : managedProfile?.defaultModel ?? readNonEmptyString(agentCoreConfig.defaultModel) ?? null, - ...(nativeRuntimeResolution.profile.backend === "claude_managed_agents_api" ? { - managedProfile: { - profileId: managedProfile!.id, - anthropicAgentId: managedProfile!.anthropicAgentId, - agentVersion: managedProfile!.agentVersion, - environmentId: managedProfile!.environmentId, - betaVersion: "managed-agents-2026-04-01" as const, - }, - maxSessionListCostUsd: Number( - parseObject(runtimeConfig).maxSessionListCostUsd - ?? managedProfile!.defaultMaxListCostCents / 100, - ), - } : {}), - ...(nativeRuntimeResolution.profile.backend === "aws_agentcore_harness_api" ? { - agentCoreProfile: { - profileId: agentCoreProfile!.id, - region: readNonEmptyString(agentCoreConfig.region) ?? "", - accountId: readNonEmptyString(agentCoreConfig.accountId) ?? "", - harnessArn: readNonEmptyString(agentCoreConfig.harnessArn) ?? "", - harnessVersion: readNonEmptyString(agentCoreConfig.harnessVersion) ?? "", - endpointArn: readNonEmptyString(agentCoreConfig.endpointArn) ?? "", - endpointQualifier: readNonEmptyString(agentCoreConfig.endpointQualifier) ?? "", - agentRuntimeArn: readNonEmptyString(agentCoreConfig.agentRuntimeArn) ?? "", - memoryArn: readNonEmptyString(agentCoreConfig.memoryArn) ?? "", - memoryId: readNonEmptyString(agentCoreConfig.memoryId) ?? "", - invocationRoleArn: readNonEmptyString(agentCoreConfig.invocationRoleArn) ?? "", - qualificationRevision: readNonEmptyString(agentCoreConfig.qualificationRevision) ?? "aws-agentcore-harness-v1", - eventExpiryDays: 90 as const, - }, - maxEstimatedSessionCostUsd: Number(parseObject(agent.adapterConfig).maxEstimatedSessionCostUsd ?? agentCoreConfig.defaultMaxEstimatedSessionCostUsd ?? 1), - invocationLimits: { - maxIterations: Math.min(8, Number(parseObject(agent.adapterConfig).maxIterations ?? 8)), - maxOutputTokens: Math.min(4096, Number(parseObject(agent.adapterConfig).maxOutputTokens ?? 4096)), - timeoutSeconds: Math.min(300, Number(parseObject(agent.adapterConfig).timeoutSeconds ?? 300)), - }, - } : {}), - lifecyclePolicy: parseObject(agent.adapterConfig).lifecycleMode === "warm" - ? { - mode: "warm", - idleTimeoutMs: Number.isSafeInteger(parseObject(agent.adapterConfig).idleTimeoutMs) - && Number(parseObject(agent.adapterConfig).idleTimeoutMs) > 0 - ? Number(parseObject(agent.adapterConfig).idleTimeoutMs) - : 300_000, - } - : { mode: "per_turn", idleTimeoutMs: null }, - interactionResponses, - completionContract: { - id: completionContract.row.id, - sha256: completionContract.row.canonicalSha256, - schemaVersion: completionContract.row.schemaVersion, - contract: completionContract.contract, - }, - }); - if (previousNativeRun && nativeSessionId === resumableTaskSessionId) { - nativeResumeCheckpoint = rebindNativeSessionCheckpoint({ - previousRun: previousNativeRun, - currentExecution: nativeExecution, - }); - if (!nativeResumeCheckpoint) { - nativeSessionId = randomUUID(); - nativeExecution = parseNativeExecutionInput({ - ...nativeExecution, - session: { - ...nativeExecution.session, - normalizedSessionId: nativeSessionId, - }, + runId: run.id, + agentId: agent.id, + interactionIds: interactionId ? [interactionId] : [], }); + const managedProfile = + nativeRuntimeResolution.profile.backend === + "claude_managed_agents_api" + ? await managedAgentProfileService(db).requireQualified( + agent.companyId, + readNonEmptyString( + parseObject(runtimeConfig).managedProfileId, + ) ?? "", + ) + : null; + const agentCoreProfile = + nativeRuntimeResolution.profile.backend === + "aws_agentcore_harness_api" + ? await remoteAgentProfileService(db).requireQualified( + agent.companyId, + readNonEmptyString( + parseObject(agent.adapterConfig).agentCoreProfileId, + ) ?? "", + "aws_bedrock_agentcore_harness", + ) + : null; + const agentCoreConfig = parseObject( + agentCoreProfile?.configuration, + ); + if (managedProfile) { + const rawApiKeyBinding = parseObject( + parseObject(agent.adapterConfig).env, + ).ANTHROPIC_API_KEY; + const boundSecretId = + typeof rawApiKeyBinding === "object" && + rawApiKeyBinding !== null + ? readNonEmptyString( + (rawApiKeyBinding as Record).secretId, + ) + : null; + if (boundSecretId !== managedProfile.apiKeySecretId) { + throw new ConfigurationIncompleteFailure( + "configuration incomplete: Claude Agent profile API key is not bound at env.ANTHROPIC_API_KEY", + { + configurationIncomplete: { + reason: "managed_agent_profile_secret_binding_mismatch", + companyId: agent.companyId, + agentId: agent.id, + profileId: managedProfile.id, + requiredEnvKeys: ["ANTHROPIC_API_KEY"], + }, + }, + ); + } + } + const executionMode = + issueRef.workMode === "planning" && !acceptedPlanContinuationWake + ? ("plan" as const) + : ("default" as const); + const pinnedPlan = + executionMode === "plan" + ? await documentService(db).getIssueDocumentByKey( + issueRef.id, + "plan", + ) + : null; + const pinnedReviewContext = + executionMode === "plan" + ? await buildPlanReviewContext({ + db, + companyId: agent.companyId, + issueId: issueRef.id, + issueWorkMode: issueRef.workMode, + interactionId: readNonEmptyString(context.interactionId), + }) + : null; + const pinnedPlanMarkdown = pinnedPlan?.body ?? ""; + nativeExecution = buildNativeExecutionInput({ + companyId: agent.companyId, + runId: run.id, + issue: issueRef, + taskPrompt: + readNonEmptyString(context.paperclipTaskMarkdown) ?? + `# ${issueRef.identifier ?? issueRef.id}: ${issueRef.title}`, + agentId: agent.id, + workspace: { + // Projectless paperclip_runner tasks still have a resolved local cwd. Bind that + // transient workspace to the run id so the native input remains durable and replayable + // without fabricating a project-scoped execution_workspaces row. + id: nativeExecutionWorkspaceId, + cwd: executionWorkspace.cwd, + repoUrl: executionWorkspace.repoUrl, + repoRef: executionWorkspace.repoRef, + branchName: executionWorkspace.branchName, + }, + normalizedSessionId: nativeSessionId, + executionMode, + planningContext: + executionMode === "plan" + ? { + documentId: pinnedPlan?.id ?? null, + baseRevisionId: pinnedPlan?.latestRevisionId ?? null, + baseRevisionNumber: pinnedPlan?.latestRevisionNumber ?? 0, + markdown: pinnedPlanMarkdown, + sha256: createHash("sha256") + .update(pinnedPlanMarkdown) + .digest("hex"), + reviewContext: pinnedReviewContext + ? (structuredClone( + pinnedReviewContext, + ) as unknown as Record) + : {}, + } + : null, + provider: + nativeRuntimeResolution.profile.backend === "opencode_server" + ? "opencode" + : nativeRuntimeResolution.profile.backend === + "claude_managed_agents_api" + ? "claude_managed" + : nativeRuntimeResolution.profile.backend === + "aws_agentcore_harness_api" + ? "aws_agentcore" + : nativeRuntimeResolution.profile.backend === + "acpx_runtime" + ? "acpx" + : "codex", + ...(nativeRuntimeResolution.profile.backend === "acpx_runtime" + ? { + acpxAgent: parseObject(runtimeConfig).acpxAgent as + "pi" | "claude" | "codex", + } + : {}), + model: + typeof parseObject(agent.adapterConfig).model === "string" + ? String(parseObject(agent.adapterConfig).model) + : (managedProfile?.defaultModel ?? + readNonEmptyString(agentCoreConfig.defaultModel) ?? + null), + ...(nativeRuntimeResolution.profile.backend === + "claude_managed_agents_api" + ? { + managedProfile: { + profileId: managedProfile!.id, + anthropicAgentId: managedProfile!.anthropicAgentId, + agentVersion: managedProfile!.agentVersion, + environmentId: managedProfile!.environmentId, + betaVersion: "managed-agents-2026-04-01" as const, + }, + maxSessionListCostUsd: Number( + parseObject(runtimeConfig).maxSessionListCostUsd ?? + managedProfile!.defaultMaxListCostCents / 100, + ), + } + : {}), + ...(nativeRuntimeResolution.profile.backend === + "aws_agentcore_harness_api" + ? { + agentCoreProfile: { + profileId: agentCoreProfile!.id, + region: readNonEmptyString(agentCoreConfig.region) ?? "", + accountId: + readNonEmptyString(agentCoreConfig.accountId) ?? "", + harnessArn: + readNonEmptyString(agentCoreConfig.harnessArn) ?? "", + harnessVersion: + readNonEmptyString(agentCoreConfig.harnessVersion) ?? + "", + endpointArn: + readNonEmptyString(agentCoreConfig.endpointArn) ?? "", + endpointQualifier: + readNonEmptyString(agentCoreConfig.endpointQualifier) ?? + "", + agentRuntimeArn: + readNonEmptyString(agentCoreConfig.agentRuntimeArn) ?? + "", + memoryArn: + readNonEmptyString(agentCoreConfig.memoryArn) ?? "", + memoryId: + readNonEmptyString(agentCoreConfig.memoryId) ?? "", + invocationRoleArn: + readNonEmptyString(agentCoreConfig.invocationRoleArn) ?? + "", + qualificationRevision: + readNonEmptyString( + agentCoreConfig.qualificationRevision, + ) ?? "aws-agentcore-harness-v1", + eventExpiryDays: 90 as const, + }, + maxEstimatedSessionCostUsd: Number( + parseObject(agent.adapterConfig) + .maxEstimatedSessionCostUsd ?? + agentCoreConfig.defaultMaxEstimatedSessionCostUsd ?? + 1, + ), + invocationLimits: { + maxIterations: Math.min( + 8, + Number( + parseObject(agent.adapterConfig).maxIterations ?? 8, + ), + ), + maxOutputTokens: Math.min( + 4096, + Number( + parseObject(agent.adapterConfig).maxOutputTokens ?? + 4096, + ), + ), + timeoutSeconds: Math.min( + 300, + Number( + parseObject(agent.adapterConfig).timeoutSeconds ?? + 300, + ), + ), + }, + } + : {}), + lifecyclePolicy: + parseObject(agent.adapterConfig).lifecycleMode === "warm" + ? { + mode: "warm", + idleTimeoutMs: + Number.isSafeInteger( + parseObject(agent.adapterConfig).idleTimeoutMs, + ) && + Number(parseObject(agent.adapterConfig).idleTimeoutMs) > + 0 + ? Number( + parseObject(agent.adapterConfig).idleTimeoutMs, + ) + : 300_000, + } + : { mode: "per_turn", idleTimeoutMs: null }, + interactionResponses, + completionContract: { + id: completionContract.row.id, + sha256: completionContract.row.canonicalSha256, + schemaVersion: completionContract.row.schemaVersion, + contract: completionContract.contract, + }, + }); + if ( + previousNativeRun && + nativeSessionId === resumableTaskSessionId + ) { + nativeResumeCheckpoint = rebindNativeSessionCheckpoint({ + previousRun: previousNativeRun, + currentExecution: nativeExecution, + }); + if (!nativeResumeCheckpoint) { + nativeSessionId = randomUUID(); + nativeExecution = parseNativeExecutionInput({ + ...nativeExecution, + session: { + ...nativeExecution.session, + normalizedSessionId: nativeSessionId, + }, + }); + } } } - } - await db.transaction(async (tx) => { - const lockedRun = await tx.select().from(heartbeatRuns) - .where(eq(heartbeatRuns.id, run.id)).for("update").limit(1) - .then((rows) => rows[0] ?? null); - if (!lockedRun) throw new Error("native_runtime_run_missing"); - if (lockedRun.runtimeModeResolvedAt && lockedRun.runtimeMode !== "native") { - throw new Error("native_runtime_mode_conflict"); - } - const lockedProfile = parseObject(lockedRun.runnerProfileJson); - await tx.update(heartbeatRuns).set({ - runtimeMode: "native", - runtimeModeResolverVersion: lockedRun.runtimeModeResolverVersion ?? nativeRuntimeResolution.resolverVersion, - runtimeModeReason: lockedRun.runtimeModeReason ?? nativeRuntimeResolution.reason, - runtimeModeResolvedAt: lockedRun.runtimeModeResolvedAt ?? new Date(), - runnerProfileJson: { - ...nativeRuntimeResolution.profile, - ...lockedProfile, - nativeExecutionInput: lockedProfile.nativeExecutionInput ?? nativeExecution, - ...(lockedProfile.sessionCheckpoint !== undefined - ? { sessionCheckpoint: lockedProfile.sessionCheckpoint } - : nativeResumeCheckpoint - ? { sessionCheckpoint: nativeResumeCheckpoint as unknown as Record } - : {}), - }, - runnerInstanceId: - previousNativeRun?.runnerInstanceId - && lockedRun.nativeSessionId !== null - && lockedRun.nativeSessionId === previousNativeRun.nativeSessionId - ? previousNativeRun.runnerInstanceId - : lockedRun.runnerInstanceId ?? nativeRunnerInstanceId, - nativeSessionId: lockedRun.nativeSessionId ?? nativeSessionId, - driverKind: lockedRun.driverKind ?? nativeExecution?.session.driverKind ?? "codex_app_server", - driverVersion: lockedRun.driverVersion ?? "phase6-v1", - completionContractId: lockedRun.completionContractId ?? completionContract.row.id, - completionContractSha256: lockedRun.completionContractSha256 ?? completionContract.row.canonicalSha256, - nativePhase: lockedRun.nativePhase ?? "observed", - nativePhaseUpdatedAt: lockedRun.nativePhaseUpdatedAt ?? new Date(), - updatedAt: new Date(), - }).where(eq(heartbeatRuns.id, run.id)); - await tx.insert(nativeRunFinalizations).values({ - runId: run.id, - companyId: agent.companyId, - issueId: issueRef.id, - phase: "observed", - }).onConflictDoNothing(); - }); - } else { - await db.update(heartbeatRuns).set({ - runtimeMode: "legacy", - runtimeModeResolverVersion: nativeRuntimeResolution.resolverVersion, - runtimeModeReason: nativeRuntimeResolution.reason, - runtimeModeResolvedAt: run.runtimeModeResolvedAt ?? new Date(), - runnerProfileJson: null, - updatedAt: new Date(), - }).where(eq(heartbeatRuns.id, run.id)); - } - const localAgentJwtScope = - issueRef?.workMode === "skill_test" - ? { kind: "skill_test" as const, issueId: issueRef.id } - : { kind: "standard" as const }; - const authToken = nativeRuntimeResolution.kind === "legacy" && adapter.supportsLocalAgentJwt - ? createLocalAgentJwt( - agent.id, - agent.companyId, - agent.adapterType, - run.id, - run.responsibleUserId, - localAgentJwtScope, - ) - : null; - if (nativeRuntimeResolution.kind === "legacy" && adapter.supportsLocalAgentJwt && !authToken) { - logger.warn( - { - companyId: agent.companyId, - agentId: agent.id, - runId: run.id, - adapterType: agent.adapterType, - }, - "local agent jwt secret missing or invalid; running without injected PAPERCLIP_API_KEY", - ); - } - let adapterFinalizeOutcome: "succeeded" | "failed" | null = null; - const inspectFinalizeWorkspaceBranch = async () => { - const workspaceRecord = persistedExecutionWorkspace?.id - ? await executionWorkspacesSvc.getById(persistedExecutionWorkspace.id) - : persistedExecutionWorkspace; - if (workspaceRecord?.strategyType !== "git_worktree") return null; - - const worktreePath = - readNonEmptyString(workspaceRecord.providerRef) ?? - readNonEmptyString(workspaceRecord.cwd) ?? - readNonEmptyString(executionWorkspace.worktreePath) ?? - readNonEmptyString(executionWorkspace.cwd); - const expectedBranchName = - readNonEmptyString(workspaceRecord.branchName) ?? - readNonEmptyString(executionWorkspace.branchName); - if (!worktreePath || !expectedBranchName) return null; - - const inspection = await inspectManagedGitWorktreeBranch({ - worktreePath, - expectedBranchName, - }); - return { workspaceRecord, inspection }; - }; - const recordWorkspaceFinalize = async ( - status: "succeeded" | "failed", - metadata?: Record, - ) => { - if (adapterFinalizeOutcome) return; - let finalizeBranchMetadata: Record | null = null; - let finalizeBranchRepairMetadata: Record | null = null; - if (status === "succeeded") { - const branchInspection = await inspectFinalizeWorkspaceBranch(); - if (branchInspection) { - let inspection = branchInspection.inspection; - const initialManagedGitWorktreeBranch = formatManagedGitWorktreeBranchInspection(inspection); - if (!inspection.valid && inspection.reasonCode === "branch_mismatch" && inspection.repoRoot) { - let repairedExpectedBranchName = inspection.expectedBranchName; - try { - const coherence = await ensureGitWorktreeBranchCoherent({ - db, - repoRoot: inspection.repoRoot, - worktreePath: inspection.worktreePath, - expectedBranchName: inspection.expectedBranchName, - actualBranchName: inspection.actualBranchName, - sourceIssue: issueRef + await db.transaction(async (tx) => { + const lockedRun = await tx + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.id, run.id)) + .for("update") + .limit(1) + .then((rows) => rows[0] ?? null); + if (!lockedRun) throw new Error("native_runtime_run_missing"); + if ( + lockedRun.runtimeModeResolvedAt && + lockedRun.runtimeMode !== "native" + ) { + throw new Error("native_runtime_mode_conflict"); + } + const lockedProfile = parseObject(lockedRun.runnerProfileJson); + await tx + .update(heartbeatRuns) + .set({ + runtimeMode: "native", + runtimeModeResolverVersion: + lockedRun.runtimeModeResolverVersion ?? + nativeRuntimeResolution.resolverVersion, + runtimeModeReason: + lockedRun.runtimeModeReason ?? nativeRuntimeResolution.reason, + runtimeModeResolvedAt: + lockedRun.runtimeModeResolvedAt ?? new Date(), + runnerProfileJson: { + ...nativeRuntimeResolution.profile, + ...lockedProfile, + ...(providerTraceRequested ? { - id: issueRef.id, - identifier: issueRef.identifier, - title: issueRef.title, - workMode: issueRef.workMode, - } - : null, - executionWorkspaceId: branchInspection.workspaceRecord.id, - heartbeatRunId: run.id, - enableWorkspaceBranchReconcileForward: - resolvedInstanceSettings.experimental.enableWorkspaceBranchReconcileForward, - enableWorkspaceDirtyQuarantineRepair: - resolvedInstanceSettings.experimental.enableWorkspaceDirtyQuarantineRepair, - persistForwardReconcile: false, - reconcileOperationPhase: "workspace_finalize", - recorder: workspaceOperationRecorder, - }); - if (coherence.branchName && coherence.branchName !== branchInspection.workspaceRecord.branchName) { - repairedExpectedBranchName = coherence.branchName; - executionWorkspace.branchName = coherence.branchName; - executionWorkspace.warnings.push(...coherence.warnings); + providerTrace: { + mode: "raw", + traceId: providerTraceCapture?.metadata.id ?? null, + maxBytes: PROVIDER_TRACE_MAX_BYTES, + }, + } + : {}), + nativeExecutionInput: + lockedProfile.nativeExecutionInput ?? nativeExecution, + ...(lockedProfile.sessionCheckpoint !== undefined + ? { sessionCheckpoint: lockedProfile.sessionCheckpoint } + : nativeResumeCheckpoint + ? { + sessionCheckpoint: + nativeResumeCheckpoint as unknown as Record< + string, + unknown + >, + } + : {}), + }, + runnerInstanceId: + previousNativeRun?.runnerInstanceId && + lockedRun.nativeSessionId !== null && + lockedRun.nativeSessionId === + previousNativeRun.nativeSessionId + ? previousNativeRun.runnerInstanceId + : (lockedRun.runnerInstanceId ?? nativeRunnerInstanceId), + nativeSessionId: lockedRun.nativeSessionId ?? nativeSessionId, + driverKind: + lockedRun.driverKind ?? + nativeExecution?.session.driverKind ?? + "codex_app_server", + driverVersion: lockedRun.driverVersion ?? "phase6-v1", + completionContractId: + lockedRun.completionContractId ?? completionContract.row.id, + completionContractSha256: + lockedRun.completionContractSha256 ?? + completionContract.row.canonicalSha256, + nativePhase: lockedRun.nativePhase ?? "observed", + nativePhaseUpdatedAt: + lockedRun.nativePhaseUpdatedAt ?? new Date(), + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, run.id)); + await tx + .insert(nativeRunFinalizations) + .values({ + runId: run.id, + companyId: agent.companyId, + issueId: issueRef.id, + phase: "observed", + }) + .onConflictDoNothing(); + }); + } else { + await db + .update(heartbeatRuns) + .set({ + runtimeMode: "legacy", + runtimeModeResolverVersion: + nativeRuntimeResolution.resolverVersion, + runtimeModeReason: nativeRuntimeResolution.reason, + runtimeModeResolvedAt: run.runtimeModeResolvedAt ?? new Date(), + runnerProfileJson: providerTraceRequested + ? { + providerTrace: { + mode: "raw", + traceId: providerTraceCapture?.metadata.id ?? null, + maxBytes: PROVIDER_TRACE_MAX_BYTES, + }, + } + : null, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, run.id)); + } + const localAgentJwtScope = + issueRef?.workMode === "skill_test" + ? { kind: "skill_test" as const, issueId: issueRef.id } + : { kind: "standard" as const }; + const authToken = + nativeRuntimeResolution.kind === "legacy" && + adapter.supportsLocalAgentJwt + ? createLocalAgentJwt( + agent.id, + agent.companyId, + agent.adapterType, + run.id, + run.responsibleUserId, + localAgentJwtScope, + ) + : null; + if ( + nativeRuntimeResolution.kind === "legacy" && + adapter.supportsLocalAgentJwt && + !authToken + ) { + logger.warn( + { + companyId: agent.companyId, + agentId: agent.id, + runId: run.id, + adapterType: agent.adapterType, + }, + "local agent jwt secret missing or invalid; running without injected PAPERCLIP_API_KEY", + ); + } + let adapterFinalizeOutcome: "succeeded" | "failed" | null = null; + const inspectFinalizeWorkspaceBranch = async () => { + const workspaceRecord = persistedExecutionWorkspace?.id + ? await executionWorkspacesSvc.getById( + persistedExecutionWorkspace.id, + ) + : persistedExecutionWorkspace; + if (workspaceRecord?.strategyType !== "git_worktree") return null; + + const worktreePath = + readNonEmptyString(workspaceRecord.providerRef) ?? + readNonEmptyString(workspaceRecord.cwd) ?? + readNonEmptyString(executionWorkspace.worktreePath) ?? + readNonEmptyString(executionWorkspace.cwd); + const expectedBranchName = + readNonEmptyString(workspaceRecord.branchName) ?? + readNonEmptyString(executionWorkspace.branchName); + if (!worktreePath || !expectedBranchName) return null; + + const inspection = await inspectManagedGitWorktreeBranch({ + worktreePath, + expectedBranchName, + }); + return { workspaceRecord, inspection }; + }; + const recordWorkspaceFinalize = async ( + status: "succeeded" | "failed", + metadata?: Record, + ) => { + if (adapterFinalizeOutcome) return; + let finalizeBranchMetadata: Record | null = null; + let finalizeBranchRepairMetadata: Record | null = + null; + if (status === "succeeded") { + const branchInspection = await inspectFinalizeWorkspaceBranch(); + if (branchInspection) { + let inspection = branchInspection.inspection; + const initialManagedGitWorktreeBranch = + formatManagedGitWorktreeBranchInspection(inspection); + if ( + !inspection.valid && + inspection.reasonCode === "branch_mismatch" && + inspection.repoRoot + ) { + let repairedExpectedBranchName = inspection.expectedBranchName; + try { + const coherence = await ensureGitWorktreeBranchCoherent({ + db, + repoRoot: inspection.repoRoot, + worktreePath: inspection.worktreePath, + expectedBranchName: inspection.expectedBranchName, + actualBranchName: inspection.actualBranchName, + sourceIssue: issueRef + ? { + id: issueRef.id, + identifier: issueRef.identifier, + title: issueRef.title, + workMode: issueRef.workMode, + } + : null, + executionWorkspaceId: branchInspection.workspaceRecord.id, + heartbeatRunId: run.id, + enableWorkspaceBranchReconcileForward: + resolvedInstanceSettings.experimental + .enableWorkspaceBranchReconcileForward, + enableWorkspaceDirtyQuarantineRepair: + resolvedInstanceSettings.experimental + .enableWorkspaceDirtyQuarantineRepair, + persistForwardReconcile: false, + reconcileOperationPhase: "workspace_finalize", + recorder: workspaceOperationRecorder, + }); + if ( + coherence.branchName && + coherence.branchName !== + branchInspection.workspaceRecord.branchName + ) { + repairedExpectedBranchName = coherence.branchName; + executionWorkspace.branchName = coherence.branchName; + executionWorkspace.warnings.push(...coherence.warnings); + } + } catch (repairErr) { + const workspaceValidationFailure = + isWorkspaceValidationFailure(repairErr) ? repairErr : null; + finalizeBranchMetadata = { + executionWorkspaceId: branchInspection.workspaceRecord.id, + ...initialManagedGitWorktreeBranch, + }; + finalizeBranchRepairMetadata = { + attempted: true, + succeeded: false, + initial: initialManagedGitWorktreeBranch, + reason: + repairErr instanceof Error + ? repairErr.message + : String(repairErr), + }; + await workspaceOperationRecorder.recordOperation({ + phase: "workspace_finalize", + cwd: executionWorkspace.cwd, + metadata: { + adapterType: agent.adapterType, + executionTargetKind: executionTarget?.kind ?? "local", + ...metadata, + managedGitWorktreeBranch: finalizeBranchMetadata, + managedGitWorktreeBranchRepair: + finalizeBranchRepairMetadata, + ...(workspaceValidationFailure?.resultJson + ? { + workspaceValidation: + workspaceValidationFailure.resultJson + .workspaceValidation ?? + workspaceValidationFailure.resultJson, + } + : {}), + }, + run: async () => ({ + status: "failed", + stderr: `Managed git worktree branch check failed: ${repairErr instanceof Error ? repairErr.message : String(repairErr)}\n`, + }), + }); + adapterFinalizeOutcome = "failed"; + throw repairErr; } - } catch (repairErr) { - const workspaceValidationFailure = isWorkspaceValidationFailure(repairErr) ? repairErr : null; - finalizeBranchMetadata = { - executionWorkspaceId: branchInspection.workspaceRecord.id, - ...initialManagedGitWorktreeBranch, - }; + + const repairedInspection = + await inspectManagedGitWorktreeBranch({ + worktreePath: inspection.worktreePath, + expectedBranchName: repairedExpectedBranchName, + repoRoot: inspection.repoRoot, + }); finalizeBranchRepairMetadata = { attempted: true, - succeeded: false, + succeeded: repairedInspection.valid, initial: initialManagedGitWorktreeBranch, - reason: repairErr instanceof Error ? repairErr.message : String(repairErr), + repaired: + formatManagedGitWorktreeBranchInspection( + repairedInspection, + ), }; + inspection = repairedInspection; + } + + const managedGitWorktreeBranch = + formatManagedGitWorktreeBranchInspection(inspection); + finalizeBranchMetadata = { + executionWorkspaceId: branchInspection.workspaceRecord.id, + ...managedGitWorktreeBranch, + }; + if (!inspection.valid) { + const workspaceValidationFingerprint = + fingerprintFinalizeWorkspaceBranchValidation({ + issueId: issueRef?.id ?? null, + executionWorkspaceId: branchInspection.workspaceRecord.id, + inspection: managedGitWorktreeBranch, + }); await workspaceOperationRecorder.recordOperation({ phase: "workspace_finalize", cwd: executionWorkspace.cwd, @@ -16301,1026 +19106,1310 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) executionTargetKind: executionTarget?.kind ?? "local", ...metadata, managedGitWorktreeBranch: finalizeBranchMetadata, - managedGitWorktreeBranchRepair: finalizeBranchRepairMetadata, - ...(workspaceValidationFailure?.resultJson - ? { workspaceValidation: workspaceValidationFailure.resultJson.workspaceValidation ?? workspaceValidationFailure.resultJson } + ...(finalizeBranchRepairMetadata + ? { + managedGitWorktreeBranchRepair: + finalizeBranchRepairMetadata, + } : {}), }, run: async () => ({ status: "failed", - stderr: `Managed git worktree branch check failed: ${repairErr instanceof Error ? repairErr.message : String(repairErr)}\n`, + stderr: `Managed git worktree branch check failed: ${inspection.reason ?? "unknown branch mismatch"}\n`, }), }); adapterFinalizeOutcome = "failed"; - throw repairErr; - } - - const repairedInspection = await inspectManagedGitWorktreeBranch({ - worktreePath: inspection.worktreePath, - expectedBranchName: repairedExpectedBranchName, - repoRoot: inspection.repoRoot, - }); - finalizeBranchRepairMetadata = { - attempted: true, - succeeded: repairedInspection.valid, - initial: initialManagedGitWorktreeBranch, - repaired: formatManagedGitWorktreeBranchInspection(repairedInspection), - }; - inspection = repairedInspection; - } - - const managedGitWorktreeBranch = formatManagedGitWorktreeBranchInspection(inspection); - finalizeBranchMetadata = { - executionWorkspaceId: branchInspection.workspaceRecord.id, - ...managedGitWorktreeBranch, - }; - if (!inspection.valid) { - const workspaceValidationFingerprint = fingerprintFinalizeWorkspaceBranchValidation({ - issueId: issueRef?.id ?? null, - executionWorkspaceId: branchInspection.workspaceRecord.id, - inspection: managedGitWorktreeBranch, - }); - await workspaceOperationRecorder.recordOperation({ - phase: "workspace_finalize", - cwd: executionWorkspace.cwd, - metadata: { - adapterType: agent.adapterType, - executionTargetKind: executionTarget?.kind ?? "local", - ...metadata, - managedGitWorktreeBranch: finalizeBranchMetadata, - ...(finalizeBranchRepairMetadata ? { managedGitWorktreeBranchRepair: finalizeBranchRepairMetadata } : {}), - }, - run: async () => ({ - status: "failed", - stderr: `Managed git worktree branch check failed: ${inspection.reason ?? "unknown branch mismatch"}\n`, - }), - }); - adapterFinalizeOutcome = "failed"; - throw new WorkspaceValidationFailure( - `Execution workspace ${branchInspection.workspaceRecord.id} expected git worktree branch "${inspection.expectedBranchName}" at "${inspection.worktreePath}", but ${inspection.reason ?? "the checked-out branch could not be verified"}. Record a sanctioned execution-workspace branch transition or restore the workspace branch before completing the run.`, - { - workspaceValidation: { - reason: "git_worktree_branch_incoherence", - fingerprint: workspaceValidationFingerprint, - adapterType: agent.adapterType, - issueId: issueRef?.id ?? null, - issueIdentifier: issueRef?.identifier ?? null, - persistedExecutionWorkspaceId: branchInspection.workspaceRecord.id, - executionWorkspaceCwd: executionWorkspace.cwd, - managedGitWorktreeBranch: finalizeBranchMetadata, + throw new WorkspaceValidationFailure( + `Execution workspace ${branchInspection.workspaceRecord.id} expected git worktree branch "${inspection.expectedBranchName}" at "${inspection.worktreePath}", but ${inspection.reason ?? "the checked-out branch could not be verified"}. Record a sanctioned execution-workspace branch transition or restore the workspace branch before completing the run.`, + { + workspaceValidation: { + reason: "git_worktree_branch_incoherence", + fingerprint: workspaceValidationFingerprint, + adapterType: agent.adapterType, + issueId: issueRef?.id ?? null, + issueIdentifier: issueRef?.identifier ?? null, + persistedExecutionWorkspaceId: + branchInspection.workspaceRecord.id, + executionWorkspaceCwd: executionWorkspace.cwd, + managedGitWorktreeBranch: finalizeBranchMetadata, + }, }, - }, + ); + } + } + } + await workspaceOperationRecorder.recordOperation({ + phase: "workspace_finalize", + cwd: executionWorkspace.cwd, + metadata: { + adapterType: agent.adapterType, + executionTargetKind: executionTarget?.kind ?? "local", + ...metadata, + ...(finalizeBranchMetadata + ? { managedGitWorktreeBranch: finalizeBranchMetadata } + : {}), + ...(finalizeBranchRepairMetadata + ? { + managedGitWorktreeBranchRepair: + finalizeBranchRepairMetadata, + } + : {}), + }, + run: async () => ({ status }), + }); + // Only mark the outcome after the row landed, so a transient write + // failure on the succeeded path can still be recovered by recording + // finalize=failed from the catch path below. + adapterFinalizeOutcome = status; + }; + + let adapterResult: AdapterExecutionResult; + try { + if (nativeRuntimeResolution.kind === "native") { + if (!nativeExecution || !nativeRunnerInstanceId) + throw new Error("native_runtime_selection_not_persisted"); + adapterResult = await executePaperclipNativeSession({ + db, + execution: nativeExecution, + runnerInstanceId: nativeRunnerInstanceId, + leaseOwner: runOptions.nativeLeaseOwner, + backend: options.nativeSessionBackendFactory?.(nativeExecution), + useRunnerd: agent.adapterType === "paperclip_runner", + onLog, + // Bootstrap the provider with executable/home discovery while + // keeping the agent's configured provider values authoritative. + runnerEnvironment: { + ...buildNativeProviderEnvironment(adapterEnv), + ...(providerTraceCapture + ? { + PAPERCLIP_PROVIDER_TRACE_PATH: providerTraceCapture.path, + PAPERCLIP_PROVIDER_TRACE_MAX_BYTES: String( + PROVIDER_TRACE_MAX_BYTES, + ), + } + : {}), + }, + onSpawn: async (meta) => { + await persistRunProcessMetadata(run.id, meta); + }, + }); + } else { + const adapterContext = { ...context }; + const runtimeMcpServers = await buildPaperclipRuntimeMcpServers({ + db, + agent, + runId: run.id, + }); + const runtimeMcp = createAdapterRuntimeMcpAccess(runtimeMcpServers); + const managedMcpConfig = await createManagedMcpRunConfig({ + db, + agent, + runId: run.id, + config: runtimeConfig, + projectId: issueRef?.projectId ?? null, + issueId: issueRef?.id ?? null, + }); + if (managedMcpConfig) { + adapterContext.paperclipManagedMcp = managedMcpConfig; + } + adapterResult = await adapter.execute({ + runId: run.id, + agent, + runtime: runtimeForAdapter, + config: runtimeConfig, + context: adapterContext, + runtimeCommandSpec: + adapter.getRuntimeCommandSpec?.(runtimeConfig) ?? null, + executionTarget, + executionTransport: remoteExecution + ? { + remoteExecution: remoteExecution as unknown as Record< + string, + unknown + >, + } + : undefined, + runtimeMcp, + onLog, + onMeta: onAdapterMeta, + onEvent: onAdapterEvent, + startupTraceContext: getStartupTraceContext(), + onRuntimeProgress: async (progress) => { + await recordCurrentHeartbeatRunRuntimeProgress( + run, + progress, + issueId, + ); + }, + onSpawn: async (meta) => { + await persistRunProcessMetadata(run.id, { + pid: meta.pid, + processGroupId: + "processGroupId" in meta && + typeof meta.processGroupId === "number" + ? meta.processGroupId + : null, + startedAt: meta.startedAt, + }); + }, + authToken: authToken ?? undefined, + }); + } + // Adapter returned cleanly, which means its workspace-restore finally + // block also ran without throwing. Record the workspace_finalize + // barrier so dependents that share this executionWorkspace can wake. + // If recording the barrier itself fails, propagate as a run failure + // rather than silently leaving dependents stranded behind a missing + // finalize row. + await recordWorkspaceFinalize("succeeded"); + if (adapterResult.nativeFinalization) { + adapterResult.nativeFinalization.workspaceFinalizeStatus = + "succeeded"; + try { + await finalizeNativeRun({ + db, + runId: run.id, + workspaceFinalizeStatus: "succeeded", + }); + } catch (finalizeErr) { + logger.warn( + { err: finalizeErr, runId: run.id }, + "native result persisted but finalization did not apply; the reconciliation loop will retry", + ); + } + } + } catch (adapterErr) { + const nativeResumeScheduled = + nativeRuntimeResolution.kind === "native" + ? await db + .select({ + phase: nativeRunFinalizations.phase, + resultId: nativeRunFinalizations.resultId, + }) + .from(nativeRunFinalizations) + .where(eq(nativeRunFinalizations.runId, run.id)) + .limit(1) + .then( + (rows) => + rows[0]?.phase === "retryable_failure" && + rows[0]?.resultId === null, + ) + : false; + if (nativeResumeScheduled) { + nativeSessionResumeScheduled = true; + throw new NativeSessionResumeScheduledError(adapterErr); + } + // Adapter (or its restore finally) threw — or the finalize record + // write itself threw. Either way the workspace may be in a partial + // state. Best-effort record finalize=failed so the dependent readiness + // check keeps the gate closed instead of waking on stale local state, + // and surface the original error to the caller. + try { + await recordWorkspaceFinalize("failed", { + errorMessage: + adapterErr instanceof Error + ? adapterErr.message + : String(adapterErr), + }); + } catch (recordErr) { + logger.warn( + { + err: recordErr, + runId: run.id, + executionWorkspaceId: persistedExecutionWorkspace?.id ?? null, + }, + "failed to record workspace_finalize=failed operation; dependents may remain gated", + ); + } + if (nativeRuntimeResolution.kind === "native") { + try { + await finalizeNativeRun({ + db, + runId: run.id, + workspaceFinalizeStatus: "failed", + }); + } catch (finalizeErr) { + logger.warn( + { err: finalizeErr, runId: run.id }, + "native result could not be marked workspace_failed; the reconciliation loop will retry persisted results", + ); + } + } + throw adapterErr; + } finally { + try { + await revokeHeartbeatRunGatewayTokens({ + db, + companyId: agent.companyId, + runId: run.id, + }); + } catch (revokeErr) { + logger.warn( + { err: revokeErr, runId: run.id, companyId: agent.companyId }, + "failed to revoke heartbeat-run MCP gateway tokens", + ); + } + } + // Reconcile the referenced-project set against the real remote staging outcome. A referenced + // project can pass authorization and clone locally at run prep, then fail to stage into the + // sandbox during execution. The run-prep observability above counts such a project as synced, + // so emit a second, stage-time line that counts each staging failure as a first-class + // `staging` failure. The synced set is the resolved referenced projects minus the ones that + // failed to stage. A run with no staging failure stays silent, so the anchor-only and + // fully-synced paths add no noise. + const referencedProjectStagingFailures = + adapterResult.referencedProjectStagingFailures ?? []; + if (referencedProjectStagingFailures.length > 0) { + const stagingFailedProjectIds = new Set( + referencedProjectStagingFailures.map( + (failure) => failure.projectId, + ), + ); + const stagedProjectObservability = + buildReferencedProjectRunObservability({ + syncedProjectIds: resolvedWorkspace.additionalWorkspaces + .map((additional) => additional.projectId) + .filter((projectId) => !stagingFailedProjectIds.has(projectId)), + failures: referencedProjectStagingFailures.map((failure) => ({ + projectId: failure.projectId, + reason: "staging" as const, + error: failure.error, + })), + }); + logger.info( + { + runId: run.id, + companyId: agent.companyId, + issueId: issueRef?.id ?? null, + ...stagedProjectObservability, + }, + "run referenced-project remote staging", + ); + } + const adapterManagedRuntimeServices = adapterResult.runtimeServices + ? await persistAdapterManagedRuntimeServices({ + db, + adapterType: agent.adapterType, + runId: run.id, + agent: { + id: agent.id, + name: agent.name, + companyId: agent.companyId, + }, + issue: issueRef, + workspace: executionWorkspace, + reports: adapterResult.runtimeServices, + }) + : []; + if (adapterManagedRuntimeServices.length > 0) { + const combinedRuntimeServices = [ + ...runtimeServices, + ...adapterManagedRuntimeServices, + ]; + context.paperclipRuntimeServices = combinedRuntimeServices; + context.paperclipRuntimePrimaryUrl = + combinedRuntimeServices.find((service) => + readNonEmptyString(service.url), + )?.url ?? null; + await db + .update(heartbeatRuns) + .set({ + contextSnapshot: context, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, run.id)); + if (issueId) { + try { + await postWorkspaceReadyComment({ + issuesSvc, + issueId, + agentId: agent.id, + runId: run.id, + workspace: executionWorkspace, + runtimeServices: adapterManagedRuntimeServices, + }); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to post adapter-managed runtime comment: ${err instanceof Error ? err.message : String(err)}\n`, ); } } } - await workspaceOperationRecorder.recordOperation({ - phase: "workspace_finalize", - cwd: executionWorkspace.cwd, - metadata: { - adapterType: agent.adapterType, - executionTargetKind: executionTarget?.kind ?? "local", - ...metadata, - ...(finalizeBranchMetadata ? { managedGitWorktreeBranch: finalizeBranchMetadata } : {}), - ...(finalizeBranchRepairMetadata ? { managedGitWorktreeBranchRepair: finalizeBranchRepairMetadata } : {}), - }, - run: async () => ({ status }), - }); - // Only mark the outcome after the row landed, so a transient write - // failure on the succeeded path can still be recovered by recording - // finalize=failed from the catch path below. - adapterFinalizeOutcome = status; - }; - - let adapterResult: AdapterExecutionResult; - try { - if (nativeRuntimeResolution.kind === "native") { - if (!nativeExecution || !nativeRunnerInstanceId) throw new Error("native_runtime_selection_not_persisted"); - adapterResult = await executePaperclipNativeSession({ - db, - execution: nativeExecution, - runnerInstanceId: nativeRunnerInstanceId, - leaseOwner: runOptions.nativeLeaseOwner, - backend: options.nativeSessionBackendFactory?.(nativeExecution), - useRunnerd: agent.adapterType === "paperclip_runner", - onLog, - // Bootstrap the provider with executable/home discovery while - // keeping the agent's configured provider values authoritative. - runnerEnvironment: buildNativeProviderEnvironment(adapterEnv), - onSpawn: async (meta) => { - await persistRunProcessMetadata(run.id, meta); - }, - }); + let outcome: RunSessionOutcome; + const latestRun = await getRun(run.id); + if (isHeartbeatRunTerminalStatus(latestRun?.status)) { + outcome = latestRun.status; + } else if (adapterResult.nativeFinalization) { + const nativeTerminal = + adapterResult.nativeFinalization.terminal.runTerminalState; + outcome = + nativeTerminal === "succeeded" + ? "succeeded" + : nativeTerminal === "cancelled" + ? "cancelled" + : "failed"; + } else if (adapterResult.timedOut) { + outcome = "timed_out"; + } else if ( + (adapterResult.exitCode ?? 0) === 0 && + !adapterResult.errorMessage + ) { + outcome = "succeeded"; } else { - const adapterContext = { ...context }; - const runtimeMcpServers = await buildPaperclipRuntimeMcpServers({ - db, - agent, - runId: run.id, - }); - const runtimeMcp = createAdapterRuntimeMcpAccess(runtimeMcpServers); - const managedMcpConfig = await createManagedMcpRunConfig({ - db, - agent, - runId: run.id, - config: runtimeConfig, - projectId: issueRef?.projectId ?? null, - issueId: issueRef?.id ?? null, - }); - if (managedMcpConfig) { - adapterContext.paperclipManagedMcp = managedMcpConfig; - } - adapterResult = await adapter.execute({ - runId: run.id, - agent, - runtime: runtimeForAdapter, - config: runtimeConfig, - context: adapterContext, - runtimeCommandSpec: adapter.getRuntimeCommandSpec?.(runtimeConfig) ?? null, - executionTarget, - executionTransport: remoteExecution - ? { remoteExecution: remoteExecution as unknown as Record } - : undefined, - runtimeMcp, - onLog, - onMeta: onAdapterMeta, - onEvent: onAdapterEvent, - startupTraceContext: getStartupTraceContext(), - onRuntimeProgress: async (progress) => { - await recordCurrentHeartbeatRunRuntimeProgress(run, progress, issueId); - }, - onSpawn: async (meta) => { - await persistRunProcessMetadata(run.id, { - pid: meta.pid, - processGroupId: - "processGroupId" in meta && typeof meta.processGroupId === "number" - ? meta.processGroupId - : null, - startedAt: meta.startedAt, - }); - }, - authToken: authToken ?? undefined, - }); + outcome = "failed"; } - // Adapter returned cleanly, which means its workspace-restore finally - // block also ran without throwing. Record the workspace_finalize - // barrier so dependents that share this executionWorkspace can wake. - // If recording the barrier itself fails, propagate as a run failure - // rather than silently leaving dependents stranded behind a missing - // finalize row. - await recordWorkspaceFinalize("succeeded"); - if (adapterResult.nativeFinalization) { - adapterResult.nativeFinalization.workspaceFinalizeStatus = "succeeded"; - try { - await finalizeNativeRun({ db, runId: run.id, workspaceFinalizeStatus: "succeeded" }); - } catch (finalizeErr) { - logger.warn( - { err: finalizeErr, runId: run.id }, - "native result persisted but finalization did not apply; the reconciliation loop will retry", - ); - } - } - } catch (adapterErr) { - const nativeResumeScheduled = nativeRuntimeResolution.kind === "native" - ? await db.select({ - phase: nativeRunFinalizations.phase, - resultId: nativeRunFinalizations.resultId, - }).from(nativeRunFinalizations) - .where(eq(nativeRunFinalizations.runId, run.id)) - .limit(1) - .then((rows) => rows[0]?.phase === "retryable_failure" && rows[0]?.resultId === null) - : false; - if (nativeResumeScheduled) { - nativeSessionResumeScheduled = true; - throw new NativeSessionResumeScheduledError(adapterErr); - } - // Adapter (or its restore finally) threw — or the finalize record - // write itself threw. Either way the workspace may be in a partial - // state. Best-effort record finalize=failed so the dependent readiness - // check keeps the gate closed instead of waking on stale local state, - // and surface the original error to the caller. - try { - await recordWorkspaceFinalize("failed", { - errorMessage: adapterErr instanceof Error ? adapterErr.message : String(adapterErr), - }); - } catch (recordErr) { - logger.warn( - { err: recordErr, runId: run.id, executionWorkspaceId: persistedExecutionWorkspace?.id ?? null }, - "failed to record workspace_finalize=failed operation; dependents may remain gated", - ); - } - if (nativeRuntimeResolution.kind === "native") { - try { - await finalizeNativeRun({ db, runId: run.id, workspaceFinalizeStatus: "failed" }); - } catch (finalizeErr) { - logger.warn( - { err: finalizeErr, runId: run.id }, - "native result could not be marked workspace_failed; the reconciliation loop will retry persisted results", - ); - } - } - throw adapterErr; - } finally { - try { - await revokeHeartbeatRunGatewayTokens({ - db, - companyId: agent.companyId, - runId: run.id, - }); - } catch (revokeErr) { - logger.warn( - { err: revokeErr, runId: run.id, companyId: agent.companyId }, - "failed to revoke heartbeat-run MCP gateway tokens", - ); - } - } - // Reconcile the referenced-project set against the real remote staging outcome. A referenced - // project can pass authorization and clone locally at run prep, then fail to stage into the - // sandbox during execution. The run-prep observability above counts such a project as synced, - // so emit a second, stage-time line that counts each staging failure as a first-class - // `staging` failure. The synced set is the resolved referenced projects minus the ones that - // failed to stage. A run with no staging failure stays silent, so the anchor-only and - // fully-synced paths add no noise. - const referencedProjectStagingFailures = adapterResult.referencedProjectStagingFailures ?? []; - if (referencedProjectStagingFailures.length > 0) { - const stagingFailedProjectIds = new Set( - referencedProjectStagingFailures.map((failure) => failure.projectId), - ); - const stagedProjectObservability = buildReferencedProjectRunObservability({ - syncedProjectIds: resolvedWorkspace.additionalWorkspaces - .map((additional) => additional.projectId) - .filter((projectId) => !stagingFailedProjectIds.has(projectId)), - failures: referencedProjectStagingFailures.map((failure) => ({ - projectId: failure.projectId, - reason: "staging" as const, - error: failure.error, - })), + + const nextSessionState = resolveNextSessionState({ + adapterType: agent.adapterType, + codec: sessionCodec, + adapterResult, + outcome, + previousParams: previousSessionParams, + previousDisplayId: runtimeForAdapter.sessionDisplayId, + previousLegacySessionId: runtimeForAdapter.sessionId, }); - logger.info( - { - runId: run.id, - companyId: agent.companyId, - issueId: issueRef?.id ?? null, - ...stagedProjectObservability, - }, - "run referenced-project remote staging", - ); - } - const adapterManagedRuntimeServices = adapterResult.runtimeServices - ? await persistAdapterManagedRuntimeServices({ - db, - adapterType: agent.adapterType, - runId: run.id, - agent: { - id: agent.id, - name: agent.name, - companyId: agent.companyId, - }, - issue: issueRef, - workspace: executionWorkspace, - reports: adapterResult.runtimeServices, - }) - : []; - if (adapterManagedRuntimeServices.length > 0) { - const combinedRuntimeServices = [ - ...runtimeServices, - ...adapterManagedRuntimeServices, - ]; - context.paperclipRuntimeServices = combinedRuntimeServices; - context.paperclipRuntimePrimaryUrl = - combinedRuntimeServices.find((service) => readNonEmptyString(service.url))?.url ?? null; - await db - .update(heartbeatRuns) - .set({ - contextSnapshot: context, - updatedAt: new Date(), - }) - .where(eq(heartbeatRuns.id, run.id)); - if (issueId) { + const rawUsage = normalizeUsageTotals(adapterResult.usage); + const sessionUsageResolution = await resolveNormalizedUsageForSession({ + agentId: agent.id, + runId: run.id, + sessionId: + nextSessionState.displayId ?? nextSessionState.legacySessionId, + rawUsage, + usageBasis: adapterResult.usageBasis ?? null, + }); + const normalizedUsage = sessionUsageResolution.normalizedUsage; + const runErrorMessage = + outcome === "cancelled" + ? (latestRun?.error ?? adapterResult.errorMessage ?? "Cancelled") + : outcome === "succeeded" + ? null + : redactCurrentUserText( + adapterResult.errorMessage ?? + (outcome === "timed_out" ? "Timed out" : "Adapter failed"), + currentUserRedactionOptions, + ); + const recordedResponsibleUserDenialCode = + normalizeResponsibleUserDenialCode(latestRun?.errorCode); + const runErrorCode = + outcome === "timed_out" + ? "timeout" + : outcome === "cancelled" + ? (latestRun?.errorCode ?? "cancelled") + : outcome === "failed" + ? (adapterResult.errorCode ?? + recordedResponsibleUserDenialCode ?? + "adapter_failed") + : null; + + let logSummary: { + bytes: number; + sha256?: string; + compressed: boolean; + } | null = null; + if (handle) { + logSummary = await runLogStore.finalize(handle); + } + const finalLogBytes = logSummary?.bytes; + if (outputProgressState.pending && typeof finalLogBytes === "number") { + outputProgressState.pending.bytes = finalLogBytes; + } + await flushOutputProgress({ force: true }); + + if (providerTraceCapture) { try { - await postWorkspaceReadyComment({ - issuesSvc, + await traceStore.finalize(run.id, run.companyId); + } catch (error) { + logger.warn( + { error, runId: run.id }, + "provider trace finalization failed without affecting run outcome", + ); + } + } + + const status = + outcome === "succeeded" + ? "succeeded" + : outcome === "cancelled" + ? "cancelled" + : outcome === "timed_out" + ? "timed_out" + : "failed"; + + const cacheAdjustedCostUsd = resolveCacheAdjustedCostUsd(adapterResult); + const usageJson = + normalizedUsage || + adapterResult.costUsd != null || + cacheAdjustedCostUsd != null + ? ({ + ...(normalizedUsage ?? {}), + ...(rawUsage + ? { + rawInputTokens: rawUsage.inputTokens, + rawCachedInputTokens: rawUsage.cachedInputTokens, + rawOutputTokens: rawUsage.outputTokens, + } + : {}), + ...(sessionUsageResolution.derivedFromSessionTotals + ? { usageSource: "session_delta" } + : adapterResult.usageBasis === "per_run" + ? { usageSource: "per_run" } + : {}), + ...((nextSessionState.displayId ?? + nextSessionState.legacySessionId) + ? { + persistedSessionId: + nextSessionState.displayId ?? + nextSessionState.legacySessionId, + } + : {}), + sessionReused: + runtimeForAdapter.sessionId != null || + runtimeForAdapter.sessionDisplayId != null, + taskSessionReused: taskSessionForRun != null, + freshSession: + runtimeForAdapter.sessionId == null && + runtimeForAdapter.sessionDisplayId == null, + sessionRotated: sessionCompaction.rotate, + sessionRotationReason: sessionCompaction.reason, + configFreshness: configFreshnessResultMetadata, + provider: + readNonEmptyString(adapterResult.provider) ?? "unknown", + biller: resolveLedgerBiller(adapterResult), + model: readNonEmptyString(adapterResult.model) ?? "unknown", + ...(adapterResult.costUsd != null + ? { costUsd: adapterResult.costUsd } + : {}), + ...(cacheAdjustedCostUsd != null + ? { cacheAdjustedCostUsd } + : {}), + costStatus: resolveLedgerCostStatus({ + costUsd: cacheAdjustedCostUsd, + inputTokens: normalizedUsage?.inputTokens ?? 0, + cachedInputTokens: normalizedUsage?.cachedInputTokens ?? 0, + outputTokens: normalizedUsage?.outputTokens ?? 0, + }), + billingType: normalizeLedgerBillingType( + adapterResult.billingType, + ), + } as Record) + : null; + + const persistedResultJson = mergeHeartbeatRunResultJson( + mergeRunStopMetadataForAgent(agent, outcome, { + resultJson: mergeModelProfileRunMetadata( + mergeAdapterRecoveryMetadata({ + resultJson: { + ...(adapterResult.nativeFinalization + ? parseObject(latestRun?.resultJson) + : {}), + ...parseObject(adapterResult.resultJson), + configFreshness: configFreshnessResultMetadata, + }, + errorFamily: adapterResult.errorFamily ?? null, + retryNotBefore: adapterResult.retryNotBefore ?? null, + }), + modelProfileApplication, + ), + errorCode: runErrorCode, + errorMessage: runErrorMessage, + }), + adapterResult.summary ?? null, + ); + + const finalRunPatch: Partial = { + finishedAt: new Date(), + error: runErrorMessage, + errorCode: runErrorCode, + exitCode: adapterResult.exitCode, + signal: adapterResult.signal, + usageJson, + resultJson: persistedResultJson, + sessionIdAfter: + nextSessionState.displayId ?? nextSessionState.legacySessionId, + stdoutExcerpt, + stderrExcerpt, + logBytes: logSummary?.bytes, + logSha256: logSummary?.sha256, + logCompressed: logSummary?.compressed ?? false, + }; + const persistedRunWrite = await setRunStatusIfRunning( + run.id, + status, + finalRunPatch, + ); + let persistedRun: typeof heartbeatRuns.$inferSelect | null = + persistedRunWrite.run; + if (!persistedRunWrite.updated) { + persistedRun = null; + // Native reconciliation can commit and project the terminal status in + // the narrow window between adapter completion and this live write. + // The status is authoritative, but it must not make us discard the + // adapter's semantic result, usage, logs, or presentation decision. + // Only complete the late metadata write when the reconciler chose the + // same terminal status; a conflicting terminal outcome remains owned + // by the path that won the compare-and-set. + if ( + adapterResult.nativeFinalization && + persistedRunWrite.run?.status === status + ) { + persistedRun = await db + .update(heartbeatRuns) + .set({ + ...finalRunPatch, + finishedAt: + persistedRunWrite.run.finishedAt ?? finalRunPatch.finishedAt, + updatedAt: new Date(), + }) + .where( + and( + eq(heartbeatRuns.id, run.id), + eq(heartbeatRuns.status, status), + ), + ) + .returning() + .then((rows) => rows[0] ?? null); + } + if (!persistedRun) { + logger.info( + { + runId: run.id, + attemptedStatus: status, + currentStatus: persistedRunWrite.run?.status ?? null, + }, + "skipping late run finalization because the run already left running state", + ); + return; + } + } + if (persistedRun) { + persistedRun = + (await classifyAndPersistRunLiveness( + persistedRun, + persistedResultJson, + )) ?? persistedRun; + } + + await setWakeupStatus( + run.wakeupRequestId, + outcome === "succeeded" ? "completed" : status, + { + finishedAt: new Date(), + error: runErrorMessage, + }, + ); + + const finalizedRun = persistedRun ?? (await getRun(run.id)); + if (finalizedRun) { + await appendRunEvent(finalizedRun, seq++, { + eventType: "lifecycle", + stream: "system", + level: outcome === "succeeded" ? "info" : "error", + message: `run ${outcome}`, + payload: { + status, + exitCode: adapterResult.exitCode, + }, + }); + try { + await completeSkillTestRunForHeartbeatOutcome({ + run: finalizedRun, issueId, - agentId: agent.id, - runId: run.id, - workspace: executionWorkspace, - runtimeServices: adapterManagedRuntimeServices, + issueWorkMode: issueRef?.workMode ?? null, + outcome, + error: runErrorMessage, }); + } catch (err) { + logger.warn( + { err, runId: finalizedRun.id, issueId }, + "failed to complete skill test run after heartbeat finalization", + ); + await onLog( + "stderr", + `[paperclip] Failed to complete skill test run: ${err instanceof Error ? err.message : String(err)}\n`, + ); + } + const livenessRun = finalizedRun; + await refreshContinuationSummaryForRun(livenessRun, agent); + const skipRunIssueComment = + parseObject(livenessRun.contextSnapshot).skipIssueComment === true; + let resolvedPresentationDecision: RunPresentationDecision | null = + null; + try { + const existingRunComment = issueId + ? await findRunIssueComment( + livenessRun.id, + livenessRun.companyId, + issueId, + persistedResultJson, + ) + : null; + const finalAgentMessage = + await findLatestCompletedFinalAgentMessage( + livenessRun.id, + livenessRun.companyId, + ); + const resolved = resolveHeartbeatRunResponse({ + resultJson: persistedResultJson, + existingComment: existingRunComment, + finalAgentMessage, + }); + let presentationDecision: RunPresentationDecision = + resolved.decision; + + if ( + issueId && + !skipRunIssueComment && + presentationDecision.commentAction === "create" && + resolved.text + ) { + const comment = await issuesSvc.addComment( + issueId, + resolved.text, + { agentId: agent.id, runId: livenessRun.id }, + ); + presentationDecision = { + ...presentationDecision, + commentId: comment.id, + reasonCodes: [ + ...presentationDecision.reasonCodes, + "resolved_response_materialized", + ], + }; + await logActivity(db, { + companyId: livenessRun.companyId, + actorType: "agent", + actorId: agent.id, + agentId: agent.id, + runId: livenessRun.id, + issueId, + action: "issue.comment_added", + entityType: "issue", + entityId: issueId, + details: { + commentId: comment.id, + bodySnippet: comment.body.slice(0, 120), + identifier: issueRef?.identifier ?? null, + issueTitle: issueRef?.title ?? null, + authorizationReason: "internal_agent_write", + source: "run_presentation_resolver", + presentationSource: presentationDecision.chosenSource, + }, + }); + } else if (presentationDecision.commentAction === "create") { + presentationDecision = { + ...presentationDecision, + commentAction: "none", + reasonCodes: [ + ...presentationDecision.reasonCodes, + skipRunIssueComment + ? "issue_comment_suppressed" + : "run_has_no_issue", + ], + }; + } + + await db + .update(heartbeatRuns) + .set({ + resultJson: { + ...persistedResultJson, + presentationDecision, + }, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, livenessRun.id)); + await appendRunEvent( + livenessRun, + await nextRunEventSeq(livenessRun.id), + { + eventType: "run.presentation.resolved", + stream: "system", + level: "info", + message: "run presentation resolved", + payload: { presentationDecision }, + }, + ); + resolvedPresentationDecision = presentationDecision; } catch (err) { await onLog( "stderr", - `[paperclip] Failed to post adapter-managed runtime comment: ${err instanceof Error ? err.message : String(err)}\n`, + `[paperclip] Failed to resolve run presentation: ${err instanceof Error ? err.message : String(err)}\n`, ); } - } - } - let outcome: RunSessionOutcome; - const latestRun = await getRun(run.id); - if (isHeartbeatRunTerminalStatus(latestRun?.status)) { - outcome = latestRun.status; - } else if (adapterResult.nativeFinalization) { - const nativeTerminal = adapterResult.nativeFinalization.terminal.runTerminalState; - outcome = nativeTerminal === "succeeded" - ? "succeeded" - : nativeTerminal === "cancelled" - ? "cancelled" - : "failed"; - } else if (adapterResult.timedOut) { - outcome = "timed_out"; - } else if ((adapterResult.exitCode ?? 0) === 0 && !adapterResult.errorMessage) { - outcome = "succeeded"; - } else { - outcome = "failed"; - } - - const nextSessionState = resolveNextSessionState({ - adapterType: agent.adapterType, - codec: sessionCodec, - adapterResult, - outcome, - previousParams: previousSessionParams, - previousDisplayId: runtimeForAdapter.sessionDisplayId, - previousLegacySessionId: runtimeForAdapter.sessionId, - }); - const rawUsage = normalizeUsageTotals(adapterResult.usage); - const sessionUsageResolution = await resolveNormalizedUsageForSession({ - agentId: agent.id, - runId: run.id, - sessionId: nextSessionState.displayId ?? nextSessionState.legacySessionId, - rawUsage, - usageBasis: adapterResult.usageBasis ?? null, - }); - const normalizedUsage = sessionUsageResolution.normalizedUsage; - const runErrorMessage = - outcome === "cancelled" - ? (latestRun?.error ?? adapterResult.errorMessage ?? "Cancelled") - : outcome === "succeeded" - ? null - : redactCurrentUserText( - adapterResult.errorMessage ?? (outcome === "timed_out" ? "Timed out" : "Adapter failed"), - currentUserRedactionOptions, - ); - const recordedResponsibleUserDenialCode = - normalizeResponsibleUserDenialCode(latestRun?.errorCode); - const runErrorCode = - outcome === "timed_out" - ? "timeout" - : outcome === "cancelled" - ? (latestRun?.errorCode ?? "cancelled") - : outcome === "failed" - ? (adapterResult.errorCode ?? recordedResponsibleUserDenialCode ?? "adapter_failed") - : null; - - let logSummary: { bytes: number; sha256?: string; compressed: boolean } | null = null; - if (handle) { - logSummary = await runLogStore.finalize(handle); - } - const finalLogBytes = logSummary?.bytes; - if (outputProgressState.pending && typeof finalLogBytes === "number") { - outputProgressState.pending.bytes = finalLogBytes; - } - await flushOutputProgress({ force: true }); - - const status = - outcome === "succeeded" - ? "succeeded" - : outcome === "cancelled" - ? "cancelled" - : outcome === "timed_out" - ? "timed_out" - : "failed"; - - const cacheAdjustedCostUsd = resolveCacheAdjustedCostUsd(adapterResult); - const usageJson = - normalizedUsage || adapterResult.costUsd != null || cacheAdjustedCostUsd != null - ? ({ - ...(normalizedUsage ?? {}), - ...(rawUsage ? { - rawInputTokens: rawUsage.inputTokens, - rawCachedInputTokens: rawUsage.cachedInputTokens, - rawOutputTokens: rawUsage.outputTokens, - } : {}), - ...(sessionUsageResolution.derivedFromSessionTotals - ? { usageSource: "session_delta" } - : adapterResult.usageBasis === "per_run" - ? { usageSource: "per_run" } - : {}), - ...((nextSessionState.displayId ?? nextSessionState.legacySessionId) - ? { persistedSessionId: nextSessionState.displayId ?? nextSessionState.legacySessionId } - : {}), - sessionReused: runtimeForAdapter.sessionId != null || runtimeForAdapter.sessionDisplayId != null, - taskSessionReused: taskSessionForRun != null, - freshSession: runtimeForAdapter.sessionId == null && runtimeForAdapter.sessionDisplayId == null, - sessionRotated: sessionCompaction.rotate, - sessionRotationReason: sessionCompaction.reason, - configFreshness: configFreshnessResultMetadata, - provider: readNonEmptyString(adapterResult.provider) ?? "unknown", - biller: resolveLedgerBiller(adapterResult), - model: readNonEmptyString(adapterResult.model) ?? "unknown", - ...(adapterResult.costUsd != null ? { costUsd: adapterResult.costUsd } : {}), - ...(cacheAdjustedCostUsd != null ? { cacheAdjustedCostUsd } : {}), - costStatus: resolveLedgerCostStatus({ - costUsd: cacheAdjustedCostUsd, - inputTokens: normalizedUsage?.inputTokens ?? 0, - cachedInputTokens: normalizedUsage?.cachedInputTokens ?? 0, - outputTokens: normalizedUsage?.outputTokens ?? 0, - }), - billingType: normalizeLedgerBillingType(adapterResult.billingType), - } as Record) - : null; - - const persistedResultJson = mergeHeartbeatRunResultJson( - mergeRunStopMetadataForAgent(agent, outcome, { - resultJson: mergeModelProfileRunMetadata( - mergeAdapterRecoveryMetadata({ - resultJson: { - ...(adapterResult.nativeFinalization ? parseObject(latestRun?.resultJson) : {}), - ...parseObject(adapterResult.resultJson), - configFreshness: configFreshnessResultMetadata, - }, - errorFamily: adapterResult.errorFamily ?? null, - retryNotBefore: adapterResult.retryNotBefore ?? null, - }), - modelProfileApplication, - ), - errorCode: runErrorCode, - errorMessage: runErrorMessage, - }), - adapterResult.summary ?? null, - ); - - const persistedRunWrite = await setRunStatusIfRunning(run.id, status, { - finishedAt: new Date(), - error: runErrorMessage, - errorCode: runErrorCode, - exitCode: adapterResult.exitCode, - signal: adapterResult.signal, - usageJson, - resultJson: persistedResultJson, - sessionIdAfter: nextSessionState.displayId ?? nextSessionState.legacySessionId, - stdoutExcerpt, - stderrExcerpt, - logBytes: logSummary?.bytes, - logSha256: logSummary?.sha256, - logCompressed: logSummary?.compressed ?? false, - }); - if (!persistedRunWrite.updated) { - logger.info( - { - runId: run.id, - attemptedStatus: status, - currentStatus: persistedRunWrite.run?.status ?? null, - }, - "skipping late run finalization because the run already left running state", - ); - return; - } - - let persistedRun = persistedRunWrite.run; - if (persistedRun) { - persistedRun = await classifyAndPersistRunLiveness(persistedRun, persistedResultJson) ?? persistedRun; - } - - await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded" ? "completed" : status, { - finishedAt: new Date(), - error: runErrorMessage, - }); - - const finalizedRun = persistedRun ?? (await getRun(run.id)); - if (finalizedRun) { - await appendRunEvent(finalizedRun, seq++, { - eventType: "lifecycle", - stream: "system", - level: outcome === "succeeded" ? "info" : "error", - message: `run ${outcome}`, - payload: { - status, - exitCode: adapterResult.exitCode, - }, - }); - try { - await completeSkillTestRunForHeartbeatOutcome({ - run: finalizedRun, - issueId, - issueWorkMode: issueRef?.workMode ?? null, - outcome, - error: runErrorMessage, - }); - } catch (err) { - logger.warn( - { err, runId: finalizedRun.id, issueId }, - "failed to complete skill test run after heartbeat finalization", - ); - await onLog( - "stderr", - `[paperclip] Failed to complete skill test run: ${err instanceof Error ? err.message : String(err)}\n`, - ); - } - const livenessRun = finalizedRun; - await refreshContinuationSummaryForRun(livenessRun, agent); - const skipRunIssueComment = parseObject(livenessRun.contextSnapshot).skipIssueComment === true; - if (issueId && outcome === "succeeded" && !skipRunIssueComment) { - try { - const existingRunComment = await findRunIssueComment( - livenessRun.id, - livenessRun.companyId, - issueId, - persistedResultJson, - ); - if (!existingRunComment) { - const issueComment = buildHeartbeatRunIssueComment(persistedResultJson); - if (issueComment) { - const comment = await issuesSvc.addComment( - issueId, - issueComment, - { agentId: agent.id, runId: livenessRun.id }, - ); - await logActivity(db, { - companyId: livenessRun.companyId, - actorType: "agent", - actorId: agent.id, - agentId: agent.id, - runId: livenessRun.id, - issueId, - action: "issue.comment_added", - entityType: "issue", - entityId: issueId, - details: { - commentId: comment.id, - bodySnippet: comment.body.slice(0, 120), - identifier: issueRef?.identifier ?? null, - issueTitle: issueRef?.title ?? null, - authorizationReason: "internal_agent_write", - source: "heartbeat_run_summary", + if (outcome === "failed" && isMaxTurnExhaustionRun(livenessRun)) { + const policy = parseMaxTurnContinuationPolicy(agent); + if (policy.enabled && policy.maxAttempts > 0) { + await scheduleBoundedRetryForRun(livenessRun, agent, { + retryReason: MAX_TURN_CONTINUATION_RETRY_REASON, + wakeReason: MAX_TURN_CONTINUATION_WAKE_REASON, + maxAttempts: policy.maxAttempts, + delayMs: policy.delayMs, + }); + } else { + await appendRunEvent( + livenessRun, + await nextRunEventSeq(livenessRun.id), + { + eventType: "lifecycle", + stream: "system", + level: "warn", + message: + "Max-turn continuation suppressed because the policy is disabled", + payload: { + retryReason: MAX_TURN_CONTINUATION_RETRY_REASON, + policy, }, + }, + ); + } + } else if ( + outcome === "failed" && + readTransientRecoveryContractFromRun(livenessRun) + ) { + await scheduleBoundedRetryForRun(livenessRun, agent); + } + const issueCommentPolicyResult = await finalizeIssueCommentPolicy( + livenessRun, + agent, + resolvedPresentationDecision, + ); + await releaseIssueExecutionAndPromote(livenessRun); + await handleRunLivenessContinuation(livenessRun); + await handleIssueReviewPathDisposition(livenessRun); + await handleSuccessfulRunHandoff( + issueCommentPolicyResult.outcome === "retry_queued" || + issueCommentPolicyResult.outcome === "retry_exhausted" + ? { + ...livenessRun, + issueCommentStatus: issueCommentPolicyResult.outcome, + } + : livenessRun, + agent, + ); + + // Dependency wake re-check: if this run's issue was marked done mid-run, + // the route-time `issue_blockers_resolved` wake may have been gated by + // workspace finalization or merged into this run. Reuse the level-triggered + // dependency backstop so finalize and periodic recovery share idempotency, + // readiness, active-path, and observability rules. + if (issueId && finalizedRun) { + try { + const blockerIssueStatus = await db + .select({ status: issues.status }) + .from(issues) + .where(eq(issues.id, issueId)) + .then((rows) => rows[0]?.status ?? null); + if (blockerIssueStatus === "done") { + await recovery.reconcileResolvedDependencyWakeBackstop({ + runId: finalizedRun.id, + companyId: finalizedRun.companyId, + blockerIssueId: issueId, + source: "workspace.finalize", }); } + } catch (finalizeWakeErr) { + logger.warn( + { err: finalizeWakeErr, runId: run.id, issueId }, + "failed to evaluate dependent wakes after workspace_finalize", + ); } - } catch (err) { - await onLog( - "stderr", - `[paperclip] Failed to post run summary comment: ${err instanceof Error ? err.message : String(err)}\n`, - ); } } - if (outcome === "failed" && isMaxTurnExhaustionRun(livenessRun)) { - const policy = parseMaxTurnContinuationPolicy(agent); - if (policy.enabled && policy.maxAttempts > 0) { - await scheduleBoundedRetryForRun(livenessRun, agent, { - retryReason: MAX_TURN_CONTINUATION_RETRY_REASON, - wakeReason: MAX_TURN_CONTINUATION_WAKE_REASON, - maxAttempts: policy.maxAttempts, - delayMs: policy.delayMs, - }); - } else { - await appendRunEvent(livenessRun, await nextRunEventSeq(livenessRun.id), { - eventType: "lifecycle", - stream: "system", - level: "warn", - message: "Max-turn continuation suppressed because the policy is disabled", - payload: { - retryReason: MAX_TURN_CONTINUATION_RETRY_REASON, - policy, - }, - }); - } - } else if (outcome === "failed" && readTransientRecoveryContractFromRun(livenessRun)) { - await scheduleBoundedRetryForRun(livenessRun, agent); - } - const issueCommentPolicyResult = await finalizeIssueCommentPolicy(livenessRun, agent); - await releaseIssueExecutionAndPromote(livenessRun); - await handleRunLivenessContinuation(livenessRun); - await handleIssueReviewPathDisposition(livenessRun); - await handleSuccessfulRunHandoff( - issueCommentPolicyResult.outcome === "retry_queued" || issueCommentPolicyResult.outcome === "retry_exhausted" - ? { - ...livenessRun, - issueCommentStatus: issueCommentPolicyResult.outcome, - } - : livenessRun, - agent, - ); - // Dependency wake re-check: if this run's issue was marked done mid-run, - // the route-time `issue_blockers_resolved` wake may have been gated by - // workspace finalization or merged into this run. Reuse the level-triggered - // dependency backstop so finalize and periodic recovery share idempotency, - // readiness, active-path, and observability rules. - if (issueId && finalizedRun) { - try { - const blockerIssueStatus = await db - .select({ status: issues.status }) - .from(issues) - .where(eq(issues.id, issueId)) - .then((rows) => rows[0]?.status ?? null); - if (blockerIssueStatus === "done") { - await recovery.reconcileResolvedDependencyWakeBackstop({ - runId: finalizedRun.id, - companyId: finalizedRun.companyId, - blockerIssueId: issueId, - source: "workspace.finalize", + if (finalizedRun) { + await updateRuntimeState( + agent, + finalizedRun, + adapterResult, + { + legacySessionId: nextSessionState.legacySessionId, + }, + normalizedUsage, + ); + if (taskKey) { + if ( + adapterResult.clearSession || + (!nextSessionState.params && !nextSessionState.displayId) + ) { + await clearTaskSessions(agent.companyId, agent.id, { + taskKey, + adapterType: agent.adapterType, + }); + } else { + await upsertTaskSession({ + companyId: agent.companyId, + agentId: agent.id, + adapterType: agent.adapterType, + taskKey, + sessionParamsJson: + attachPaperclipSessionMetadataToSessionParams( + nextSessionState.params, + configuredModel, + sessionConfigMetadata, + ), + sessionDisplayId: nextSessionState.displayId, + lastRunId: finalizedRun.id, + lastError: runErrorMessage, }); } - } catch (finalizeWakeErr) { + } + } + await finalizeAgentStatus(agent.id, outcome, runErrorMessage, { + keepIdleOnFailure: + outcome === "failed" && + ((finalizedRun + ? readHeartbeatRunErrorFamily(finalizedRun) === "provider_quota" + : runErrorCode === "provider_quota") || + isWorkspaceSyncConflictFailure(adapterResult.errorMessage)), + wasFirstHeartbeat: timerClaimWasFirstHeartbeat(run), + }); + } catch (err) { + if (err instanceof NativeSessionResumeScheduledError) { + const coordinator = await db + .select({ + nextAttemptAt: nativeRunFinalizations.nextAttemptAt, + attempt: nativeRunFinalizations.attempt, + }) + .from(nativeRunFinalizations) + .where(eq(nativeRunFinalizations.runId, run.id)) + .limit(1) + .then((rows) => rows[0] ?? null); + await appendRunEvent(run, seq++, { + eventType: "lifecycle", + stream: "system", + level: "warn", + message: + "native session transport interrupted; same-run resume persisted", + payload: { + attempt: coordinator?.attempt ?? null, + nextAttemptAt: coordinator?.nextAttemptAt?.toISOString() ?? null, + fallbackSuppressed: true, + }, + }).catch(() => undefined); + return; + } + const message = redactCurrentUserText( + err instanceof Error ? err.message : "Unknown adapter failure", + await getCurrentUserRedactionOptions(), + ); + const workspaceValidationFailure = isWorkspaceValidationFailure(err) + ? err + : null; + const configurationIncompleteFailure = isConfigurationIncompleteFailure( + err, + ) + ? err + : null; + const recordedResponsibleUserDenialCode = + normalizeResponsibleUserDenialCode( + (await getRun(run.id).catch(() => null))?.errorCode, + ); + const failureErrorCode = + workspaceValidationFailure?.code ?? + configurationIncompleteFailure?.code ?? + recordedResponsibleUserDenialCode ?? + "adapter_failed"; + logger.error({ err, runId }, "heartbeat execution failed"); + + let logSummary: { + bytes: number; + sha256?: string; + compressed: boolean; + } | null = null; + if (handle) { + try { + logSummary = await runLogStore.finalize(handle); + } catch (finalizeErr) { logger.warn( - { err: finalizeWakeErr, runId: run.id, issueId }, - "failed to evaluate dependent wakes after workspace_finalize", + { err: finalizeErr, runId }, + "failed to finalize run log after error", ); } } - } + const finalLogBytes = logSummary?.bytes; + if (outputProgressState.pending && typeof finalLogBytes === "number") { + outputProgressState.pending.bytes = finalLogBytes; + } + await flushOutputProgress({ force: true }).catch((flushErr) => { + logger.warn( + { err: flushErr, runId }, + "failed to flush run output progress after error", + ); + }); - if (finalizedRun) { - await updateRuntimeState(agent, finalizedRun, adapterResult, { - legacySessionId: nextSessionState.legacySessionId, - }, normalizedUsage); - if (taskKey) { - if (adapterResult.clearSession || (!nextSessionState.params && !nextSessionState.displayId)) { - await clearTaskSessions(agent.companyId, agent.id, { - taskKey, - adapterType: agent.adapterType, + const failedRunWrite = await setRunStatusIfRunning(run.id, "failed", { + error: message, + errorCode: failureErrorCode, + finishedAt: new Date(), + resultJson: mergeRunStopMetadataForAgent(agent, "failed", { + errorCode: failureErrorCode, + errorMessage: message, + resultJson: + workspaceValidationFailure?.resultJson ?? + configurationIncompleteFailure?.resultJson ?? + null, + }), + stdoutExcerpt, + stderrExcerpt, + logBytes: logSummary?.bytes, + logSha256: logSummary?.sha256, + logCompressed: logSummary?.compressed ?? false, + }); + if (!failedRunWrite.updated) { + logger.info( + { + runId: run.id, + attemptedStatus: "failed", + currentStatus: failedRunWrite.run?.status ?? null, + }, + "skipping late adapter failure finalization because the run already left running state", + ); + return; + } + + const failedRun = failedRunWrite.run; + await setWakeupStatus(run.wakeupRequestId, "failed", { + finishedAt: new Date(), + error: message, + }); + + if (failedRun) { + await appendRunEvent(failedRun, seq++, { + eventType: "error", + stream: "system", + level: "error", + message, + }); + const livenessRun = + (await classifyAndPersistRunLiveness(failedRun)) ?? failedRun; + try { + await completeSkillTestRunForHeartbeatOutcome({ + run: livenessRun, + issueId, + issueWorkMode: issueRef?.workMode ?? null, + outcome: "failed", + error: message, }); - } else { + } catch (err) { + logger.warn( + { err, runId: livenessRun.id, issueId }, + "failed to complete skill test run after heartbeat adapter failure", + ); + } + await refreshContinuationSummaryForRun(livenessRun, agent); + if ( + !isWorkspaceValidationFailedRun(livenessRun) && + !isConfigurationIncompleteFailedRun(livenessRun) + ) { + await finalizeIssueCommentPolicy(livenessRun, agent); + } + await scheduleInteractionContinuationInfrastructureRetryIfEligible( + livenessRun, + agent, + ); + await releaseIssueExecutionAndPromote(livenessRun); + await handleIssueReviewPathDisposition(livenessRun); + + await updateRuntimeState( + agent, + livenessRun, + { + exitCode: null, + signal: null, + timedOut: false, + errorMessage: message, + }, + { + legacySessionId: runtimeForAdapter.sessionId, + }, + ); + + if ( + taskKey && + (previousSessionParams || previousSessionDisplayId || taskSession) + ) { await upsertTaskSession({ companyId: agent.companyId, agentId: agent.id, adapterType: agent.adapterType, taskKey, sessionParamsJson: attachPaperclipSessionMetadataToSessionParams( - nextSessionState.params, + previousSessionParams, configuredModel, sessionConfigMetadata, ), - sessionDisplayId: nextSessionState.displayId, - lastRunId: finalizedRun.id, - lastError: runErrorMessage, + sessionDisplayId: previousSessionDisplayId, + lastRunId: failedRun.id, + lastError: message, }); } } - } - await finalizeAgentStatus( - agent.id, - outcome, - runErrorMessage, - { - keepIdleOnFailure: - outcome === "failed" && - ((finalizedRun ? readHeartbeatRunErrorFamily(finalizedRun) === "provider_quota" : runErrorCode === "provider_quota") || - isWorkspaceSyncConflictFailure(adapterResult.errorMessage)), + + await finalizeAgentStatus(agent.id, "failed", message, { wasFirstHeartbeat: timerClaimWasFirstHeartbeat(run), - }, - ); - } catch (err) { - if (err instanceof NativeSessionResumeScheduledError) { - const coordinator = await db.select({ - nextAttemptAt: nativeRunFinalizations.nextAttemptAt, - attempt: nativeRunFinalizations.attempt, - }).from(nativeRunFinalizations) - .where(eq(nativeRunFinalizations.runId, run.id)) - .limit(1) - .then((rows) => rows[0] ?? null); - await appendRunEvent(run, seq++, { - eventType: "lifecycle", - stream: "system", - level: "warn", - message: "native session transport interrupted; same-run resume persisted", - payload: { - attempt: coordinator?.attempt ?? null, - nextAttemptAt: coordinator?.nextAttemptAt?.toISOString() ?? null, - fallbackSuppressed: true, - }, - }).catch(() => undefined); - return; - } - const message = redactCurrentUserText( - err instanceof Error ? err.message : "Unknown adapter failure", - await getCurrentUserRedactionOptions(), - ); - const workspaceValidationFailure = isWorkspaceValidationFailure(err) ? err : null; - const configurationIncompleteFailure = isConfigurationIncompleteFailure(err) ? err : null; - const recordedResponsibleUserDenialCode = - normalizeResponsibleUserDenialCode((await getRun(run.id).catch(() => null))?.errorCode); - const failureErrorCode = - workspaceValidationFailure?.code - ?? configurationIncompleteFailure?.code - ?? recordedResponsibleUserDenialCode - ?? "adapter_failed"; - logger.error({ err, runId }, "heartbeat execution failed"); - - let logSummary: { bytes: number; sha256?: string; compressed: boolean } | null = null; - if (handle) { - try { - logSummary = await runLogStore.finalize(handle); - } catch (finalizeErr) { - logger.warn({ err: finalizeErr, runId }, "failed to finalize run log after error"); - } - } - const finalLogBytes = logSummary?.bytes; - if (outputProgressState.pending && typeof finalLogBytes === "number") { - outputProgressState.pending.bytes = finalLogBytes; - } - await flushOutputProgress({ force: true }).catch((flushErr) => { - logger.warn({ err: flushErr, runId }, "failed to flush run output progress after error"); - }); - - const failedRunWrite = await setRunStatusIfRunning(run.id, "failed", { - error: message, - errorCode: failureErrorCode, - finishedAt: new Date(), - resultJson: mergeRunStopMetadataForAgent(agent, "failed", { - errorCode: failureErrorCode, - errorMessage: message, - resultJson: workspaceValidationFailure?.resultJson ?? configurationIncompleteFailure?.resultJson ?? null, - }), - stdoutExcerpt, - stderrExcerpt, - logBytes: logSummary?.bytes, - logSha256: logSummary?.sha256, - logCompressed: logSummary?.compressed ?? false, - }); - if (!failedRunWrite.updated) { - logger.info( - { - runId: run.id, - attemptedStatus: "failed", - currentStatus: failedRunWrite.run?.status ?? null, - }, - "skipping late adapter failure finalization because the run already left running state", - ); - return; - } - - const failedRun = failedRunWrite.run; - await setWakeupStatus(run.wakeupRequestId, "failed", { - finishedAt: new Date(), - error: message, - }); - - if (failedRun) { - await appendRunEvent(failedRun, seq++, { - eventType: "error", - stream: "system", - level: "error", - message, + keepIdleOnFailure: isWorkspaceSyncConflictFailure(message), }); - const livenessRun = await classifyAndPersistRunLiveness(failedRun) ?? failedRun; - try { - await completeSkillTestRunForHeartbeatOutcome({ - run: livenessRun, - issueId, - issueWorkMode: issueRef?.workMode ?? null, - outcome: "failed", - error: message, - }); - } catch (err) { - logger.warn( - { err, runId: livenessRun.id, issueId }, - "failed to complete skill test run after heartbeat adapter failure", - ); - } - await refreshContinuationSummaryForRun(livenessRun, agent); - if (!isWorkspaceValidationFailedRun(livenessRun) && !isConfigurationIncompleteFailedRun(livenessRun)) { - await finalizeIssueCommentPolicy(livenessRun, agent); - } - await scheduleInteractionContinuationInfrastructureRetryIfEligible(livenessRun, agent); - await releaseIssueExecutionAndPromote(livenessRun); - await handleIssueReviewPathDisposition(livenessRun); - - await updateRuntimeState(agent, livenessRun, { - exitCode: null, - signal: null, - timedOut: false, - errorMessage: message, - }, { - legacySessionId: runtimeForAdapter.sessionId, - }); - - if (taskKey && (previousSessionParams || previousSessionDisplayId || taskSession)) { - await upsertTaskSession({ - companyId: agent.companyId, - agentId: agent.id, - adapterType: agent.adapterType, - taskKey, - sessionParamsJson: attachPaperclipSessionMetadataToSessionParams( - previousSessionParams, - configuredModel, - sessionConfigMetadata, - ), - sessionDisplayId: previousSessionDisplayId, - lastRunId: failedRun.id, - lastError: message, - }); - } } - - await finalizeAgentStatus(agent.id, "failed", message, { - wasFirstHeartbeat: timerClaimWasFirstHeartbeat(run), - keepIdleOnFailure: isWorkspaceSyncConflictFailure(message), - }); - } } catch (outerErr) { - if (isWorkspaceBusyDeferral(outerErr)) { - // Expected contention on a shared project workspace, not a - // failure: park the run as a bounded scheduled retry and leave the - // holder undisturbed. The finally block below still releases - // leases, runtime services, and scratch for this run. - await finalizeWorkspaceBusyDeferral(run, outerErr).catch((deferralErr) => { - logger.error( - { err: deferralErr, runId }, - "failed to finalize workspace-busy deferral", + if (isWorkspaceBusyDeferral(outerErr)) { + // Expected contention on a shared project workspace, not a + // failure: park the run as a bounded scheduled retry and leave the + // holder undisturbed. The finally block below still releases + // leases, runtime services, and scratch for this run. + await finalizeWorkspaceBusyDeferral(run, outerErr).catch( + (deferralErr) => { + logger.error( + { err: deferralErr, runId }, + "failed to finalize workspace-busy deferral", + ); + }, + ); + } else { + // Setup code before adapter.execute threw (e.g. ensureRuntimeState, resolveWorkspaceForRun). + // The inner catch did not fire, so we must record the failure here. + const message = redactCurrentUserText( + outerErr instanceof Error + ? outerErr.message + : "Unknown setup failure", + await getCurrentUserRedactionOptions(), + ); + // A missing secret/env binding is a known pre-dispatch configuration gap, + // not an opaque setup crash. Surface it with its own errorCode so the + // recovery path routes it to a human owner instead of looping retries. + const workspaceValidationSetupFailure = isWorkspaceValidationFailure( + outerErr, + ) + ? outerErr + : null; + const configurationIncompleteSetupFailure = + isConfigurationIncompleteFailure(outerErr) ? outerErr : null; + const recordedResponsibleUserDenialCode = + normalizeResponsibleUserDenialCode( + (await getRun(runId).catch(() => null))?.errorCode, + ); + const setupFailureErrorCode = + workspaceValidationSetupFailure?.code ?? + configurationIncompleteSetupFailure?.code ?? + recordedResponsibleUserDenialCode ?? + "setup_failed"; + logger.error( + { err: outerErr, runId }, + "heartbeat execution setup failed", + ); + const setupFailureAgent = await getAgent(run.agentId).catch(() => null); + const setupFailureWrite = await setRunStatusIfRunning(runId, "failed", { + error: message, + errorCode: setupFailureErrorCode, + finishedAt: new Date(), + ...(setupFailureAgent + ? { + resultJson: mergeRunStopMetadataForAgent( + setupFailureAgent, + "failed", + { + errorCode: setupFailureErrorCode, + errorMessage: message, + resultJson: + workspaceValidationSetupFailure?.resultJson ?? + configurationIncompleteSetupFailure?.resultJson ?? + null, + }, + ), + } + : {}), + }).catch(() => ({ run: null, updated: false as const })); + if (!setupFailureWrite.updated) { + logger.info( + { + runId, + attemptedStatus: "failed", + currentStatus: setupFailureWrite.run?.status ?? null, + }, + "skipping late setup failure finalization because the run already left running state", + ); + } else { + await setWakeupStatus(run.wakeupRequestId, "failed", { + finishedAt: new Date(), + error: message, + }).catch(() => undefined); + } + const failedRun = await getRun(runId).catch(() => null); + if (setupFailureWrite.updated && failedRun) { + // Emit a run-log event so the failure is visible in the run timeline, + // consistent with what the inner catch block does for adapter failures. + await appendRunEvent(failedRun, 1, { + eventType: "error", + stream: "system", + level: "error", + message, + }).catch(() => undefined); + const livenessRun = await classifyAndPersistRunLiveness( + failedRun, + ).catch(() => failedRun); + const setupFailureIssueId = readNonEmptyString( + parseObject(livenessRun.contextSnapshot).issueId, + ); + if (setupFailureIssueId) { + await completeSkillTestRunForHeartbeatOutcome({ + run: livenessRun, + issueId: setupFailureIssueId, + outcome: "failed", + error: message, + }).catch((completionErr) => { + logger.warn( + { + err: completionErr, + runId: livenessRun.id, + issueId: setupFailureIssueId, + }, + "failed to complete skill test run after heartbeat setup failure", ); }); - } else { - // Setup code before adapter.execute threw (e.g. ensureRuntimeState, resolveWorkspaceForRun). - // The inner catch did not fire, so we must record the failure here. - const message = redactCurrentUserText( - outerErr instanceof Error ? outerErr.message : "Unknown setup failure", - await getCurrentUserRedactionOptions(), - ); - // A missing secret/env binding is a known pre-dispatch configuration gap, - // not an opaque setup crash. Surface it with its own errorCode so the - // recovery path routes it to a human owner instead of looping retries. - const workspaceValidationSetupFailure = isWorkspaceValidationFailure(outerErr) ? outerErr : null; - const configurationIncompleteSetupFailure = isConfigurationIncompleteFailure(outerErr) ? outerErr : null; - const recordedResponsibleUserDenialCode = - normalizeResponsibleUserDenialCode((await getRun(runId).catch(() => null))?.errorCode); - const setupFailureErrorCode = - workspaceValidationSetupFailure?.code ?? - configurationIncompleteSetupFailure?.code ?? - recordedResponsibleUserDenialCode ?? - "setup_failed"; - logger.error({ err: outerErr, runId }, "heartbeat execution setup failed"); - const setupFailureAgent = await getAgent(run.agentId).catch(() => null); - const setupFailureWrite = await setRunStatusIfRunning(runId, "failed", { - error: message, - errorCode: setupFailureErrorCode, - finishedAt: new Date(), - ...(setupFailureAgent ? { - resultJson: mergeRunStopMetadataForAgent(setupFailureAgent, "failed", { - errorCode: setupFailureErrorCode, - errorMessage: message, - resultJson: - workspaceValidationSetupFailure?.resultJson ?? configurationIncompleteSetupFailure?.resultJson ?? null, - }), - } : {}), - }).catch(() => ({ run: null, updated: false as const })); - if (!setupFailureWrite.updated) { - logger.info( - { - runId, - attemptedStatus: "failed", - currentStatus: setupFailureWrite.run?.status ?? null, - }, - "skipping late setup failure finalization because the run already left running state", - ); - } else { - await setWakeupStatus(run.wakeupRequestId, "failed", { - finishedAt: new Date(), - error: message, - }).catch(() => undefined); } - const failedRun = await getRun(runId).catch(() => null); - if (setupFailureWrite.updated && failedRun) { - // Emit a run-log event so the failure is visible in the run timeline, - // consistent with what the inner catch block does for adapter failures. - await appendRunEvent(failedRun, 1, { - eventType: "error", - stream: "system", - level: "error", - message, - }).catch(() => undefined); - const livenessRun = await classifyAndPersistRunLiveness(failedRun).catch(() => failedRun); - const setupFailureIssueId = readNonEmptyString(parseObject(livenessRun.contextSnapshot).issueId); - if (setupFailureIssueId) { - await completeSkillTestRunForHeartbeatOutcome({ - run: livenessRun, - issueId: setupFailureIssueId, - outcome: "failed", - error: message, - }).catch((completionErr) => { - logger.warn( - { err: completionErr, runId: livenessRun.id, issueId: setupFailureIssueId }, - "failed to complete skill test run after heartbeat setup failure", - ); - }); + const failedAgent = + setupFailureAgent ?? + (await getAgent(run.agentId).catch(() => null)); + if (failedAgent) { + await refreshContinuationSummaryForRun( + livenessRun, + failedAgent, + ).catch(() => undefined); + if ( + !isWorkspaceValidationFailedRun(livenessRun) && + !isConfigurationIncompleteFailedRun(livenessRun) + ) { + await finalizeIssueCommentPolicy(livenessRun, failedAgent).catch( + () => undefined, + ); } - const failedAgent = setupFailureAgent ?? await getAgent(run.agentId).catch(() => null); - if (failedAgent) { - await refreshContinuationSummaryForRun(livenessRun, failedAgent).catch(() => undefined); - if (!isWorkspaceValidationFailedRun(livenessRun) && !isConfigurationIncompleteFailedRun(livenessRun)) { - await finalizeIssueCommentPolicy(livenessRun, failedAgent).catch(() => undefined); - } - await scheduleInteractionContinuationInfrastructureRetryIfEligible(livenessRun, failedAgent).catch((retryError) => { - logger.warn( - { err: retryError, runId: livenessRun.id }, - "failed to schedule interaction continuation retry after setup failure", - ); - }); - } - await releaseIssueExecutionAndPromote(livenessRun).catch((releaseError) => { + await scheduleInteractionContinuationInfrastructureRetryIfEligible( + livenessRun, + failedAgent, + ).catch((retryError) => { + logger.warn( + { err: retryError, runId: livenessRun.id }, + "failed to schedule interaction continuation retry after setup failure", + ); + }); + } + await releaseIssueExecutionAndPromote(livenessRun).catch( + (releaseError) => { logger.error( { err: releaseError, runId }, "failed to release issue execution after heartbeat setup failure", ); - }); - await handleIssueReviewPathDisposition(livenessRun).catch((reviewPathError) => { + }, + ); + await handleIssueReviewPathDisposition(livenessRun).catch( + (reviewPathError) => { logger.error( { err: reviewPathError, runId }, "failed to evaluate review-path disposition after heartbeat setup failure", ); - }); - } - // Ensure the agent is not left stuck in "running" if the setup-failure - // path owned the terminal transition. If another path already finalized - // the run, keep that terminal outcome authoritative. - if (setupFailureWrite.updated) { - await finalizeAgentStatus(run.agentId, "failed", message, { - wasFirstHeartbeat: timerClaimWasFirstHeartbeat(run), - }).catch(() => undefined); - } - } - } finally { - let latestRun = await getRun(run.id).catch(() => null); - // Close the invariant "environment lease released implies the run is - // terminal". When the teardown reaches this point with the run still - // running or queued, force a terminal status before the lease is - // released, so the UI never shows a finished task as "Live". - if (latestRun && !nativeSessionResumeScheduled) { - latestRun = await terminalizeRunOnLeaseRelease(latestRun).catch((terminalizeErr) => { - logger.error( - { err: terminalizeErr, runId: run.id }, - "failed to terminalize run before environment lease release", - ); - return latestRun; - }); - } - if (!nativeSessionResumeScheduled) { - await releaseEnvironmentLeasesForRun({ - runId: run.id, - companyId: run.companyId, - agentId: run.agentId, - status: latestRun?.status, - failureReason: latestRun?.error ?? undefined, - }); - await releaseRuntimeServicesForRun(run.id).catch(() => undefined); - } - if (runScratch && latestRun && isHeartbeatRunTerminalStatus(latestRun.status)) { - const scratchForCleanup = runScratch; - let scratchCleanup: Awaited> | null = null; - try { - scratchCleanup = await cleanupHeartbeatRunScratch({ - scratch: scratchForCleanup, - processGroupId: latestRun.processGroupId, - isProcessGroupAlive, - }); - } catch (scratchCleanupError) { - logger.warn( - { - err: scratchCleanupError, - runId: run.id, - scratchDir: scratchForCleanup.dir, - }, - "failed to clean heartbeat run scratch directory", - ); - await appendRunEvent(latestRun, await nextRunEventSeq(latestRun.id), { - eventType: "error", - stream: "system", - level: "warn", - message: "run scratch cleanup failed", - payload: { - dir: scratchForCleanup.dir, - error: scratchCleanupError instanceof Error - ? scratchCleanupError.message - : String(scratchCleanupError), - }, - }).catch(() => undefined); - } - if (scratchCleanup) { - await appendRunEvent(latestRun, await nextRunEventSeq(latestRun.id), { - eventType: "lifecycle", - stream: "system", - level: scratchCleanup.removed ? "info" : "warn", - message: scratchCleanup.removed - ? "run scratch cleaned" - : `run scratch cleanup skipped: ${scratchCleanup.reason}`, - payload: scratchCleanup, - }).catch((scratchCleanupEventError) => { - logger.warn( - { - err: scratchCleanupEventError, - runId: run.id, - scratchDir: scratchForCleanup.dir, - }, - "failed to record heartbeat run scratch cleanup event", - ); - }); - } - } - activeRunExecutions.delete(run.id); - if (!nativeSessionResumeScheduled) { - await startNextQueuedRunForAgent(run.agentId); - } + }, + ); } + // Ensure the agent is not left stuck in "running" if the setup-failure + // path owned the terminal transition. If another path already finalized + // the run, keep that terminal outcome authoritative. + if (setupFailureWrite.updated) { + await finalizeAgentStatus(run.agentId, "failed", message, { + wasFirstHeartbeat: timerClaimWasFirstHeartbeat(run), + }).catch(() => undefined); + } + } + } finally { + let latestRun = await getRun(run.id).catch(() => null); + // Close the invariant "environment lease released implies the run is + // terminal". When the teardown reaches this point with the run still + // running or queued, force a terminal status before the lease is + // released, so the UI never shows a finished task as "Live". + if (latestRun && !nativeSessionResumeScheduled) { + latestRun = await terminalizeRunOnLeaseRelease(latestRun).catch( + (terminalizeErr) => { + logger.error( + { err: terminalizeErr, runId: run.id }, + "failed to terminalize run before environment lease release", + ); + return latestRun; + }, + ); + } + if (!nativeSessionResumeScheduled) { + await releaseEnvironmentLeasesForRun({ + runId: run.id, + companyId: run.companyId, + agentId: run.agentId, + status: latestRun?.status, + failureReason: latestRun?.error ?? undefined, + }); + await releaseRuntimeServicesForRun(run.id).catch(() => undefined); + } + if ( + runScratch && + latestRun && + isHeartbeatRunTerminalStatus(latestRun.status) + ) { + const scratchForCleanup = runScratch; + let scratchCleanup: Awaited< + ReturnType + > | null = null; + try { + scratchCleanup = await cleanupHeartbeatRunScratch({ + scratch: scratchForCleanup, + processGroupId: latestRun.processGroupId, + isProcessGroupAlive, + }); + } catch (scratchCleanupError) { + logger.warn( + { + err: scratchCleanupError, + runId: run.id, + scratchDir: scratchForCleanup.dir, + }, + "failed to clean heartbeat run scratch directory", + ); + await appendRunEvent(latestRun, await nextRunEventSeq(latestRun.id), { + eventType: "error", + stream: "system", + level: "warn", + message: "run scratch cleanup failed", + payload: { + dir: scratchForCleanup.dir, + error: + scratchCleanupError instanceof Error + ? scratchCleanupError.message + : String(scratchCleanupError), + }, + }).catch(() => undefined); + } + if (scratchCleanup) { + await appendRunEvent(latestRun, await nextRunEventSeq(latestRun.id), { + eventType: "lifecycle", + stream: "system", + level: scratchCleanup.removed ? "info" : "warn", + message: scratchCleanup.removed + ? "run scratch cleaned" + : `run scratch cleanup skipped: ${scratchCleanup.reason}`, + payload: scratchCleanup, + }).catch((scratchCleanupEventError) => { + logger.warn( + { + err: scratchCleanupEventError, + runId: run.id, + scratchDir: scratchForCleanup.dir, + }, + "failed to record heartbeat run scratch cleanup event", + ); + }); + } + } + activeRunExecutions.delete(run.id); + if (!nativeSessionResumeScheduled) { + await startNextQueuedRunForAgent(run.agentId); + } + } } async function releaseIssueExecutionAndPromote( @@ -17388,7 +20477,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eq(issues.executionRunId, run.id), eq(issues.checkoutRunId, run.id), ) - : or(eq(issues.executionRunId, run.id), eq(issues.checkoutRunId, run.id)), + : or( + eq(issues.executionRunId, run.id), + eq(issues.checkoutRunId, run.id), + ), ), ) .orderBy(asc(issues.id)); @@ -17414,7 +20506,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) updatedAt: promotionUpdateTimestamp, }) .where( - and(eq(issues.companyId, run.companyId), eq(issues.executionRunId, run.id)), + and( + eq(issues.companyId, run.companyId), + eq(issues.executionRunId, run.id), + ), ); // `checkoutRunId` clear is symmetric to #6008's per-issue self-heal, // extended to all siblings: covers paths where the issue's assignee or @@ -17427,7 +20522,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) updatedAt: promotionUpdateTimestamp, }) .where( - and(eq(issues.companyId, run.companyId), eq(issues.checkoutRunId, run.id)), + and( + eq(issues.companyId, run.companyId), + eq(issues.checkoutRunId, run.id), + ), ); // Deferred-wake promotion is bound to a single primary issue: the run's context @@ -17446,7 +20544,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // Sibling lock cleanup is already done above; only the primary issue carries // the recovery surface because the comment is attached to a single issue. if ( - (isWorkspaceValidationFailedRun(run) || isConfigurationIncompleteFailedRun(run)) && + (isWorkspaceValidationFailedRun(run) || + isConfigurationIncompleteFailedRun(run)) && (issue.status === "todo" || issue.status === "in_progress") && !issue.assigneeUserId && issue.assigneeAgentId === run.agentId @@ -17465,7 +20564,6 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; } - while (true) { const deferred = await tx .select() @@ -17491,28 +20589,33 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const companyAgents = deferredAgent ? await tx - .select({ - id: agents.id, - companyId: agents.companyId, - name: agents.name, - reportsTo: agents.reportsTo, - status: agents.status, - }) - .from(agents) - .where(eq(agents.companyId, issue.companyId)) + .select({ + id: agents.id, + companyId: agents.companyId, + name: agents.name, + reportsTo: agents.reportsTo, + status: agents.status, + }) + .from(agents) + .where(eq(agents.companyId, issue.companyId)) : []; const deferredInvokability = deferredAgent?.companyId === issue.companyId ? evaluateAgentInvokability(deferredAgent, companyAgents) : evaluateAgentInvokability(null, companyAgents); - if (!deferredAgent || deferredAgent.companyId !== issue.companyId || !deferredInvokability.invokable) { + if ( + !deferredAgent || + deferredAgent.companyId !== issue.companyId || + !deferredInvokability.invokable + ) { await tx .update(agentWakeupRequests) .set({ status: "failed", finishedAt: new Date(), - error: "Deferred wake could not be promoted: agent is not invokable", + error: + "Deferred wake could not be promoted: agent is not invokable", updatedAt: new Date(), }) .where(eq(agentWakeupRequests.id, deferred.id)); @@ -17520,16 +20623,23 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const deferredPayload = parseObject(deferred.payload); - const deferredContextSeed = parseObject(deferredPayload[DEFERRED_WAKE_CONTEXT_KEY]); - const activePauseHold = await treeControlSvc.getActivePauseHoldGate(issue.companyId, issue.id); - const treeHoldInteractionWake = activePauseHold && await isVerifiedIssueTreeControlInteractionWake(tx, { - companyId: issue.companyId, - issueId: issue.id, - agentId: deferred.agentId, - contextSnapshot: deferredContextSeed, - requestedByActorType: deferred.requestedByActorType, - requestedByActorId: deferred.requestedByActorId, - }); + const deferredContextSeed = parseObject( + deferredPayload[DEFERRED_WAKE_CONTEXT_KEY], + ); + const activePauseHold = await treeControlSvc.getActivePauseHoldGate( + issue.companyId, + issue.id, + ); + const treeHoldInteractionWake = + activePauseHold && + (await isVerifiedIssueTreeControlInteractionWake(tx, { + companyId: issue.companyId, + issueId: issue.id, + agentId: deferred.agentId, + contextSnapshot: deferredContextSeed, + requestedByActorType: deferred.requestedByActorType, + requestedByActorId: deferred.requestedByActorId, + })); if (activePauseHold && !treeHoldInteractionWake) { await tx .update(agentWakeupRequests) @@ -17543,7 +20653,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) continue; } - const promotedContextSeed: Record = { ...deferredContextSeed }; + const promotedContextSeed: Record = { + ...deferredContextSeed, + }; if (activePauseHold) { promotedContextSeed.treeHoldInteraction = true; promotedContextSeed.activeTreeHold = { @@ -17556,7 +20668,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; } const deferredCommentIds = extractWakeCommentIds(deferredContextSeed); - const deferredWakeReason = readNonEmptyString(deferredContextSeed.wakeReason); + const deferredWakeReason = readNonEmptyString( + deferredContextSeed.wakeReason, + ); // Local-CLI agents post comments under user auth, so a self-comment from // the run that is now ending would otherwise look like a real human // comment and trigger a reopen on the very issue this run just closed. @@ -17577,7 +20691,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .then((rows) => rows); deferredCommentWakeIsSelfAuthored = deferredComments.length > 0 && - deferredComments.every((comment) => comment.createdByRunId === run.id); + deferredComments.every( + (comment) => comment.createdByRunId === run.id, + ); } // Only human/comment-reopen interactions should revive completed issues; // system follow-ups such as retry or cleanup wakes must not reopen closed work. @@ -17585,10 +20701,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) deferredCommentIds.length > 0 && !deferredCommentWakeIsSelfAuthored && (issue.status === "done" || issue.status === "cancelled") && - ( - deferred.requestedByActorType === "user" || - deferredWakeReason === "issue_reopened_via_comment" - ); + (deferred.requestedByActorType === "user" || + deferredWakeReason === "issue_reopened_via_comment"); let reopenedActivity: LogActivityInput | null = null; if (shouldReopenDeferredCommentWake) { @@ -17631,11 +20745,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } - const promotedReason = readNonEmptyString(deferred.reason) ?? "issue_execution_promoted"; + const promotedReason = + readNonEmptyString(deferred.reason) ?? "issue_execution_promoted"; const promotedSource = - (readNonEmptyString(deferred.source) as WakeupOptions["source"]) ?? "automation"; + (readNonEmptyString(deferred.source) as WakeupOptions["source"]) ?? + "automation"; const promotedTriggerDetail = - (readNonEmptyString(deferred.triggerDetail) as WakeupOptions["triggerDetail"]) ?? null; + (readNonEmptyString( + deferred.triggerDetail, + ) as WakeupOptions["triggerDetail"]) ?? null; const promotedPayload = deferredPayload; delete promotedPayload[DEFERRED_WAKE_CONTEXT_KEY]; @@ -17652,30 +20770,41 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const sessionBefore = readNonEmptyString(promotedContextSnapshot.resumeSessionDisplayId) ?? - await resolveSessionBeforeForWakeup(deferredAgent, promotedTaskKey); + (await resolveSessionBeforeForWakeup(deferredAgent, promotedTaskKey)); const promotedContinuationAttempt = readContinuationAttempt( promotedContextSnapshot.livenessContinuationAttempt, ); - const promotedResponsibleUserId = await resolveResponsibleUserIdForRunSeed({ - companyId: deferredAgent.companyId, - contextSnapshot: promotedContextSnapshot, - issueContext: issue, - routineEnvContext: await getRoutineEnvForExecutionIssue(deferredAgent.companyId, issue), - requestedByActorType: deferred.requestedByActorType as "user" | "agent" | "system" | null, - requestedByActorId: deferred.requestedByActorId, - source: promotedSource, - triggerDetail: promotedTriggerDetail, - existingRunResponsibleUserId: run.responsibleUserId, - }); - if (!promotedResponsibleUserId) { - throw new HttpError(422, "Unable to resolve responsible user for promoted heartbeat run", { - code: "responsible_user_unresolved", - runId: run.id, - agentId: deferredAgent.id, + const promotedResponsibleUserId = + await resolveResponsibleUserIdForRunSeed({ companyId: deferredAgent.companyId, - issueId: issue.id, - wakeReason: readNonEmptyString(promotedContextSnapshot.wakeReason), + contextSnapshot: promotedContextSnapshot, + issueContext: issue, + routineEnvContext: await getRoutineEnvForExecutionIssue( + deferredAgent.companyId, + issue, + ), + requestedByActorType: deferred.requestedByActorType as + "user" | "agent" | "system" | null, + requestedByActorId: deferred.requestedByActorId, + source: promotedSource, + triggerDetail: promotedTriggerDetail, + existingRunResponsibleUserId: run.responsibleUserId, }); + if (!promotedResponsibleUserId) { + throw new HttpError( + 422, + "Unable to resolve responsible user for promoted heartbeat run", + { + code: "responsible_user_unresolved", + runId: run.id, + agentId: deferredAgent.id, + companyId: deferredAgent.companyId, + issueId: issue.id, + wakeReason: readNonEmptyString( + promotedContextSnapshot.wakeReason, + ), + }, + ); } const now = new Date(); const newRun = await tx @@ -17717,7 +20846,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) updatedAt: now, }) // Promoted mention wakes are issue-scoped, not issue ownership transfers. - .where(and(eq(issues.id, issue.id), eq(issues.assigneeAgentId, deferredAgent.id))); + .where( + and( + eq(issues.id, issue.id), + eq(issues.assigneeAgentId, deferredAgent.id), + ), + ); return { kind: "promoted" as const, @@ -17733,7 +20867,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .where( and( eq(heartbeatRuns.companyId, issue.companyId), - inArray(heartbeatRuns.status, [...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES]), + inArray(heartbeatRuns.status, [ + ...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES, + ]), sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`, sql`${heartbeatRuns.id} <> ${run.id}`, agentId ? eq(heartbeatRuns.agentId, agentId) : sql`true`, @@ -17761,9 +20897,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .limit(1) .then((rows) => rows[0] ?? null); const executionState = parseIssueExecutionState(issue.executionState); - const currentParticipant = executionState?.status === "pending" - ? executionState.currentParticipant - : null; + const currentParticipant = + executionState?.status === "pending" + ? executionState.currentParticipant + : null; const issueNeedsReviewParticipantRecovery = issue.status === "in_review" && !issue.assigneeUserId && @@ -17775,12 +20912,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ); if (issueNeedsReviewParticipantRecovery) { - const existingReviewParticipantExecutionPath = await findExistingExecutionPath(currentParticipant.agentId); + const existingReviewParticipantExecutionPath = + await findExistingExecutionPath(currentParticipant.agentId); if ( options.suppressImmediateRecovery || existingReviewParticipantExecutionPath || issueHasPersistedMonitor || - await isAutomaticRecoverySuppressedByPauseHold(db, issue.companyId, issue.id, treeControlSvc) + (await isAutomaticRecoverySuppressedByPauseHold( + db, + issue.companyId, + issue.id, + treeControlSvc, + )) ) { return { kind: "released" as const }; } @@ -17817,13 +20960,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "automation", triggerDetail: "system", reason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_WAKE_REASON, - payload: withRecoveryModelProfileHint({ - issueId: issue.id, - retryOfRunId: run.id, - retryReason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON, - currentStageId: executionState?.currentStageId ?? null, - currentStageType: executionState?.currentStageType ?? null, - }, "normal_model"), + payload: withRecoveryModelProfileHint( + { + issueId: issue.id, + retryOfRunId: run.id, + retryReason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON, + currentStageId: executionState?.currentStageId ?? null, + currentStageType: executionState?.currentStageType ?? null, + }, + "normal_model", + ), status: "queued", requestedByActorType: "system", requestedByActorId: null, @@ -17841,18 +20987,21 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: "system", status: "queued", wakeupRequestId: wakeupRequest.id, - contextSnapshot: withRecoveryModelProfileHint({ - issueId: issue.id, - taskId: issue.id, - wakeReason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_WAKE_REASON, - retryReason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON, - source: "issue.execution_review_recovery", - retryOfRunId: run.id, - currentStageId: executionState?.currentStageId ?? null, - currentStageType: executionState?.currentStageType ?? null, - reviewRecoveryInstruction: - "The previous reviewer run ended while this execution-review stage was still pending. Submit the review decision now, or mark the issue blocked with the exact unblock action.", - }, "normal_model"), + contextSnapshot: withRecoveryModelProfileHint( + { + issueId: issue.id, + taskId: issue.id, + wakeReason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_WAKE_REASON, + retryReason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON, + source: "issue.execution_review_recovery", + retryOfRunId: run.id, + currentStageId: executionState?.currentStageId ?? null, + currentStageType: executionState?.currentStageType ?? null, + reviewRecoveryInstruction: + "The previous reviewer run ended while this execution-review stage was still pending. Submit the review decision now, or mark the issue blocked with the exact unblock action.", + }, + "normal_model", + ), sessionIdBefore: recoverySessionBefore, retryOfRunId: run.id, updatedAt: now, @@ -17888,7 +21037,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) (issue.status === "todo" || issue.status === "in_progress") && !issue.assigneeUserId && issue.assigneeAgentId === run.agentId && - (run.status === "failed" || run.status === "timed_out" || run.status === "cancelled"); + (run.status === "failed" || + run.status === "timed_out" || + run.status === "cancelled"); if (!issueNeedsImmediateRecovery) { return { kind: "released" as const }; @@ -17898,11 +21049,22 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const existingExecutionPath = await findExistingExecutionPath(); - if (existingExecutionPath || issueHasPersistedMonitor || await findExplicitBlockerPath()) { + if ( + existingExecutionPath || + issueHasPersistedMonitor || + (await findExplicitBlockerPath()) + ) { return { kind: "released" as const }; } - if (await isAutomaticRecoverySuppressedByPauseHold(db, issue.companyId, issue.id, treeControlSvc)) { + if ( + await isAutomaticRecoverySuppressedByPauseHold( + db, + issue.companyId, + issue.id, + treeControlSvc, + ) + ) { return { kind: "released" as const }; } @@ -17919,10 +21081,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) !recoveryAgent || isWorkspaceValidationFailedRun(run) || isConfigurationIncompleteFailedRun(run) || - didAutomaticRecoveryFail(run, issue.status === "todo" ? "assignment_recovery" : "issue_continuation_needed"); + didAutomaticRecoveryFail( + run, + issue.status === "todo" + ? "assignment_recovery" + : "issue_continuation_needed", + ); if (shouldBlockImmediately) { const workspaceValidationFailure = isWorkspaceValidationFailedRun(run); - const configurationIncompleteFailure = isConfigurationIncompleteFailedRun(run); + const configurationIncompleteFailure = + isConfigurationIncompleteFailedRun(run); const notice = workspaceValidationFailure ? buildWorkspaceValidationRecoveryNoticeSeed() : configurationIncompleteFailure @@ -17943,24 +21111,38 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; } - const retryReason = issue.status === "todo" ? "assignment_recovery" : "issue_continuation_needed"; - const recoveryReason = issue.status === "todo" ? "issue_assignment_recovery" : "issue_continuation_needed"; + const retryReason = + issue.status === "todo" + ? "assignment_recovery" + : "issue_continuation_needed"; + const recoveryReason = + issue.status === "todo" + ? "issue_assignment_recovery" + : "issue_continuation_needed"; const recoverySource = - issue.status === "todo" ? "issue.assignment_recovery" : "issue.continuation_recovery"; + issue.status === "todo" + ? "issue.assignment_recovery" + : "issue.continuation_recovery"; const now = new Date(); - const recoveryContextSnapshot = withRecoveryModelProfileHint({ - issueId: issue.id, - taskId: issue.id, - wakeReason: recoveryReason, - retryReason, - source: recoverySource, - retryOfRunId: run.id, - }, "normal_model"); + const recoveryContextSnapshot = withRecoveryModelProfileHint( + { + issueId: issue.id, + taskId: issue.id, + wakeReason: recoveryReason, + retryReason, + source: recoverySource, + retryOfRunId: run.id, + }, + "normal_model", + ); const responsibleUserId = await resolveResponsibleUserIdForRunSeed({ companyId: issue.companyId, contextSnapshot: recoveryContextSnapshot, issueContext: issue, - routineEnvContext: await getRoutineEnvForExecutionIssue(issue.companyId, issue), + routineEnvContext: await getRoutineEnvForExecutionIssue( + issue.companyId, + issue, + ), requestedByActorType: "system", requestedByActorId: null, source: "automation", @@ -17968,14 +21150,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) existingRunResponsibleUserId: run.responsibleUserId, }); if (!responsibleUserId) { - throw new HttpError(422, "Unable to resolve responsible user for recovery heartbeat run", { - code: "responsible_user_unresolved", - runId: run.id, - agentId: recoveryAgent.id, - companyId: issue.companyId, - issueId: issue.id, - wakeReason: recoveryReason, - }); + throw new HttpError( + 422, + "Unable to resolve responsible user for recovery heartbeat run", + { + code: "responsible_user_unresolved", + runId: run.id, + agentId: recoveryAgent.id, + companyId: issue.companyId, + issueId: issue.id, + wakeReason: recoveryReason, + }, + ); } const wakeupRequest = await tx .insert(agentWakeupRequests) @@ -17985,10 +21171,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "automation", triggerDetail: "system", reason: recoveryReason, - payload: withRecoveryModelProfileHint({ - issueId: issue.id, - retryOfRunId: run.id, - }, "normal_model"), + payload: withRecoveryModelProfileHint( + { + issueId: issue.id, + retryOfRunId: run.id, + }, + "normal_model", + ), status: "queued", requestedByActorType: "system", requestedByActorId: null, @@ -18042,17 +21231,20 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (promotionResult?.kind === "blocked") { await recovery.escalateStrandedAssignedIssue({ issue: promotionResult.issue, - previousStatus: promotionResult.previousStatus as "todo" | "in_progress" | "in_review", + previousStatus: promotionResult.previousStatus as + "todo" | "in_progress" | "in_review", latestRun: run, notice: promotionResult.notice, recoveryCause: promotionResult.recoveryCause === WORKSPACE_VALIDATION_RECOVERY_CAUSE ? WORKSPACE_VALIDATION_RECOVERY_CAUSE - : promotionResult.recoveryCause === CONFIGURATION_INCOMPLETE_RECOVERY_CAUSE + : promotionResult.recoveryCause === + CONFIGURATION_INCOMPLETE_RECOVERY_CAUSE ? CONFIGURATION_INCOMPLETE_RECOVERY_CAUSE - : promotionResult.recoveryCause === EXECUTION_REVIEW_PARTICIPANT_RECOVERY_CAUSE + : promotionResult.recoveryCause === + EXECUTION_REVIEW_PARTICIPANT_RECOVERY_CAUSE ? EXECUTION_REVIEW_PARTICIPANT_RECOVERY_CAUSE - : undefined, + : undefined, recoveryOwnerAgentId: promotionResult.recoveryOwnerAgentId, }); return; @@ -18061,7 +21253,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (promotionResult?.kind === "blocked_recovery_in_place") { await recovery.escalateStrandedRecoveryIssueInPlace({ issue: promotionResult.issue, - previousStatus: promotionResult.previousStatus as "todo" | "in_progress" | "in_review", + previousStatus: promotionResult.previousStatus as + "todo" | "in_progress" | "in_review", latestRun: run, }); return; @@ -18070,7 +21263,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const promotedRun = promotionResult?.run ?? null; if (!promotedRun) return; - if (promotionResult?.kind === "promoted" && promotionResult.reopenedActivity) { + if ( + promotionResult?.kind === "promoted" && + promotionResult.reopenedActivity + ) { await logActivity(db, promotionResult.reopenedActivity); } @@ -18092,7 +21288,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) async function enqueueWakeup(agentId: string, opts: WakeupOptions = {}) { const source = opts.source ?? "on_demand"; const triggerDetail = opts.triggerDetail ?? null; - const contextSnapshot: Record = { ...(opts.contextSnapshot ?? {}) }; + const contextSnapshot: Record = { + ...(opts.contextSnapshot ?? {}), + }; const reason = opts.reason ?? null; const payload = opts.payload ?? null; const { @@ -18107,11 +21305,27 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail, payload, }); - let issueId = readNonEmptyString(enrichedContextSnapshot.issueId) ?? issueIdFromPayload; + let issueId = + readNonEmptyString(enrichedContextSnapshot.issueId) ?? issueIdFromPayload; const agent = await getAgent(agentId); if (!agent) throw notFound("Agent not found"); + const agentDebug = parseObject(parseObject(agent.runtimeConfig).debug); + const runDebug = parseObject(enrichedContextSnapshot.debug); + if ( + agentDebug.providerTrace === "raw" + && runDebug.providerTrace !== "raw" + ) { + enrichedContextSnapshot.debug = { + ...runDebug, + providerTrace: "raw", + }; + enrichedContextSnapshot.providerTraceRequestedBy = + `agent:${agent.id}:debug-setting`; + enrichedContextSnapshot.providerTraceRequestSource = "agent_debug_setting"; + } + const writeSkippedRequest = async ( skipReason: string, patch: Partial = {}, @@ -18131,7 +21345,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ...patch, }); }; - const writeSkippedHeartbeatRequest = async (skipReason: string, details: Record) => { + const writeSkippedHeartbeatRequest = async ( + skipReason: string, + details: Record, + ) => { await writeSkippedRequest(skipReason, { payload: { ...(payload ?? {}), @@ -18148,9 +21365,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return null; } - const worktreeExecutionCutoff = opts.requestedByActorType === "user" - ? null - : await getWorktreeExecutionCutoff(); + const worktreeExecutionCutoff = + opts.requestedByActorType === "user" + ? null + : await getWorktreeExecutionCutoff(); const company = await db .select({ status: companies.status }) @@ -18169,29 +21387,47 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return null; } - const explicitResumeSession = await resolveExplicitResumeSessionOverride(agent, payload, taskKey); + const explicitResumeSession = await resolveExplicitResumeSessionOverride( + agent, + payload, + taskKey, + ); if (explicitResumeSession) { - enrichedContextSnapshot.resumeFromRunId = explicitResumeSession.resumeFromRunId; - enrichedContextSnapshot.resumeSessionDisplayId = explicitResumeSession.sessionDisplayId; - enrichedContextSnapshot.resumeSessionParams = explicitResumeSession.sessionParams; - if (!readNonEmptyString(enrichedContextSnapshot.issueId) && explicitResumeSession.issueId) { + enrichedContextSnapshot.resumeFromRunId = + explicitResumeSession.resumeFromRunId; + enrichedContextSnapshot.resumeSessionDisplayId = + explicitResumeSession.sessionDisplayId; + enrichedContextSnapshot.resumeSessionParams = + explicitResumeSession.sessionParams; + if ( + !readNonEmptyString(enrichedContextSnapshot.issueId) && + explicitResumeSession.issueId + ) { enrichedContextSnapshot.issueId = explicitResumeSession.issueId; } - if (!readNonEmptyString(enrichedContextSnapshot.taskId) && explicitResumeSession.taskId) { + if ( + !readNonEmptyString(enrichedContextSnapshot.taskId) && + explicitResumeSession.taskId + ) { enrichedContextSnapshot.taskId = explicitResumeSession.taskId; } - if (!readNonEmptyString(enrichedContextSnapshot.taskKey) && explicitResumeSession.taskKey) { + if ( + !readNonEmptyString(enrichedContextSnapshot.taskKey) && + explicitResumeSession.taskKey + ) { enrichedContextSnapshot.taskKey = explicitResumeSession.taskKey; } issueId = readNonEmptyString(enrichedContextSnapshot.issueId) ?? issueId; } - const effectiveTaskKey = readNonEmptyString(enrichedContextSnapshot.taskKey) ?? taskKey; + const effectiveTaskKey = + readNonEmptyString(enrichedContextSnapshot.taskKey) ?? taskKey; const sessionBefore = explicitResumeSession?.sessionDisplayId ?? - await resolveSessionBeforeForWakeup(agent, effectiveTaskKey); + (await resolveSessionBeforeForWakeup(agent, effectiveTaskKey)); let hasResolvablePriorSessionWorkspace: boolean | null = null; const resolveHasResolvablePriorSessionWorkspace = async () => { - if (hasResolvablePriorSessionWorkspace !== null) return hasResolvablePriorSessionWorkspace; + if (hasResolvablePriorSessionWorkspace !== null) + return hasResolvablePriorSessionWorkspace; hasResolvablePriorSessionWorkspace = issueId ? await hasResolvablePriorSessionWorkspaceForWake({ agent, @@ -18202,7 +21438,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) : false; return hasResolvablePriorSessionWorkspace; }; - const continuationAttempt = readContinuationAttempt(enrichedContextSnapshot.livenessContinuationAttempt); + const continuationAttempt = readContinuationAttempt( + enrichedContextSnapshot.livenessContinuationAttempt, + ); let projectId = readNonEmptyString(enrichedContextSnapshot.projectId); if (!projectId && issueId) { @@ -18214,20 +21452,33 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) // evaluated. const lookupIsUuid = isUuidLike(issueId); const idMatch = lookupIsUuid - ? or(eq(issues.id, issueId), eq(issues.identifier, issueId.toUpperCase())) + ? or( + eq(issues.id, issueId), + eq(issues.identifier, issueId.toUpperCase()), + ) : eq(issues.identifier, issueId.toUpperCase()); const resolvedIssue = await db - .select({ id: issues.id, projectId: issues.projectId, createdAt: issues.createdAt }) + .select({ + id: issues.id, + projectId: issues.projectId, + createdAt: issues.createdAt, + }) .from(issues) .where(and(eq(issues.companyId, agent.companyId), idMatch)) .then((rows) => rows[0] ?? null); if (resolvedIssue) { - if (worktreeExecutionCutoff && resolvedIssue.createdAt < worktreeExecutionCutoff) { - await writeSkippedHeartbeatRequest("heartbeat.worktree_execution_cutoff", { - reason: "worktree_execution_cutoff", - cutoff: worktreeExecutionCutoff.toISOString(), - issueId: resolvedIssue.id, - }); + if ( + worktreeExecutionCutoff && + resolvedIssue.createdAt < worktreeExecutionCutoff + ) { + await writeSkippedHeartbeatRequest( + "heartbeat.worktree_execution_cutoff", + { + reason: "worktree_execution_cutoff", + cutoff: worktreeExecutionCutoff.toISOString(), + issueId: resolvedIssue.id, + }, + ); return null; } projectId = resolvedIssue.projectId ?? null; @@ -18252,38 +21503,54 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) let queuedResponsibleUserIdPromise: Promise | null = null; const resolveQueuedResponsibleUserId = () => { queuedResponsibleUserIdPromise ??= (async () => { - const queuedIssueContext = issueId ? await getIssueExecutionContext(agent.companyId, issueId) : null; - const queuedRoutineEnvContext = await getRoutineEnvForExecutionIssue(agent.companyId, queuedIssueContext); - const queuedResponsibleUserId = await resolveResponsibleUserIdForRunSeed({ - companyId: agent.companyId, - contextSnapshot: enrichedContextSnapshot, - issueContext: queuedIssueContext, - routineEnvContext: queuedRoutineEnvContext, - requestedByActorType: opts.requestedByActorType ?? null, - requestedByActorId: opts.requestedByActorId ?? null, - source, - triggerDetail, - }); - if (!queuedResponsibleUserId) { - throw new HttpError(422, "Unable to resolve responsible user for heartbeat run dispatch", { - code: "responsible_user_unresolved", - agentId, + const queuedIssueContext = issueId + ? await getIssueExecutionContext(agent.companyId, issueId) + : null; + const queuedRoutineEnvContext = await getRoutineEnvForExecutionIssue( + agent.companyId, + queuedIssueContext, + ); + const queuedResponsibleUserId = + await resolveResponsibleUserIdForRunSeed({ companyId: agent.companyId, - issueId: issueId ?? null, + contextSnapshot: enrichedContextSnapshot, + issueContext: queuedIssueContext, + routineEnvContext: queuedRoutineEnvContext, + requestedByActorType: opts.requestedByActorType ?? null, + requestedByActorId: opts.requestedByActorId ?? null, source, triggerDetail, - wakeReason: readNonEmptyString(enrichedContextSnapshot.wakeReason), }); + if (!queuedResponsibleUserId) { + throw new HttpError( + 422, + "Unable to resolve responsible user for heartbeat run dispatch", + { + code: "responsible_user_unresolved", + agentId, + companyId: agent.companyId, + issueId: issueId ?? null, + source, + triggerDetail, + wakeReason: readNonEmptyString( + enrichedContextSnapshot.wakeReason, + ), + }, + ); } return queuedResponsibleUserId; })(); return queuedResponsibleUserIdPromise; }; - const budgetBlock = await budgets.getInvocationBlock(agent.companyId, agentId, { - issueId, - projectId, - }); + const budgetBlock = await budgets.getInvocationBlock( + agent.companyId, + agentId, + { + issueId, + projectId, + }, + ); if (budgetBlock) { await writeSkippedRequest("budget.blocked"); throw conflict(budgetBlock.reason, { @@ -18324,25 +21591,34 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) !wakeCommentId && !readNonEmptyString(enrichedContextSnapshot.taskId) && !readNonEmptyString(enrichedContextSnapshot.taskKey); - if (policy.skipTimerWhenNoActionableWork && genericTimerWake && !(await hasActionableTimerWork(agent))) { + if ( + policy.skipTimerWhenNoActionableWork && + genericTimerWake && + !(await hasActionableTimerWork(agent)) + ) { await writeSkippedHeartbeatRequest("heartbeat.timer.no_actionable_work", { - reason: "No assigned todo or in_progress issue requires this agent before timer adapter invocation.", + reason: + "No assigned todo or in_progress issue requires this agent before timer adapter invocation.", }); await markTimerHeartbeatChecked(agentId, source); return null; } if (issueId) { - const activePauseHold = await treeControlSvc.getActivePauseHoldGate(agent.companyId, issueId); + const activePauseHold = await treeControlSvc.getActivePauseHoldGate( + agent.companyId, + issueId, + ); if (activePauseHold) { - const treeHoldInteractionWake = await isVerifiedIssueTreeControlInteractionWake(db, { - companyId: agent.companyId, - issueId, - agentId, - contextSnapshot: enrichedContextSnapshot, - requestedByActorType: opts.requestedByActorType, - requestedByActorId: opts.requestedByActorId, - }); + const treeHoldInteractionWake = + await isVerifiedIssueTreeControlInteractionWake(db, { + companyId: agent.companyId, + issueId, + agentId, + contextSnapshot: enrichedContextSnapshot, + requestedByActorType: opts.requestedByActorType, + requestedByActorId: opts.requestedByActorId, + }); if (!treeHoldInteractionWake) { await writeSkippedRequest("issue_tree_hold_active"); @@ -18361,7 +21637,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) requestedReason: reason, source, triggerDetail, - securityPrinciples: ["Complete Mediation", "Fail Securely", "Secure Defaults"], + securityPrinciples: [ + "Complete Mediation", + "Fail Securely", + "Secure Defaults", + ], }, }); return null; @@ -18407,7 +21687,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) createdAt: issues.createdAt, }) .from(issues) - .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId))) + .where( + and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)), + ) .then((rows) => rows[0] ?? null); if (!issue) { @@ -18427,7 +21709,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { kind: "skipped" as const }; } - if (worktreeExecutionCutoff && issue.createdAt < worktreeExecutionCutoff) { + if ( + worktreeExecutionCutoff && + issue.createdAt < worktreeExecutionCutoff + ) { await tx.insert(agentWakeupRequests).values({ companyId: agent.companyId, agentId, @@ -18451,7 +21736,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { kind: "skipped" as const }; } - const cancelStaleScheduledRetry = async (scheduledRun: typeof heartbeatRuns.$inferSelect) => { + const cancelStaleScheduledRetry = async ( + scheduledRun: typeof heartbeatRuns.$inferSelect, + ) => { const issueCancelled = issue.status === "cancelled"; if ( scheduledRun.status !== "scheduled_retry" || @@ -18470,10 +21757,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) status: "cancelled", finishedAt: now, error: reason, - errorCode: issueCancelled ? "issue_cancelled" : "issue_reassigned", + errorCode: issueCancelled + ? "issue_cancelled" + : "issue_reassigned", updatedAt: now, }) - .where(and(eq(heartbeatRuns.id, scheduledRun.id), eq(heartbeatRuns.status, "scheduled_retry"))) + .where( + and( + eq(heartbeatRuns.id, scheduledRun.id), + eq(heartbeatRuns.status, "scheduled_retry"), + ), + ) .returning() .then((rows) => rows[0] ?? null); @@ -18500,7 +21794,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) executionLockedAt: null, updatedAt: now, }) - .where(and(eq(issues.id, issue.id), eq(issues.executionRunId, scheduledRun.id))); + .where( + and( + eq(issues.id, issue.id), + eq(issues.executionRunId, scheduledRun.id), + ), + ); } const eventSeq = Number(cancelled.nextEventSeq ?? 1); @@ -18519,7 +21818,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) issueId: issue.id, issueStatus: issue.status, scheduledRetryAttempt: cancelled.scheduledRetryAttempt, - scheduledRetryAt: cancelled.scheduledRetryAt ? new Date(cancelled.scheduledRetryAt).toISOString() : null, + scheduledRetryAt: cancelled.scheduledRetryAt + ? new Date(cancelled.scheduledRetryAt).toISOString() + : null, scheduledRetryReason: cancelled.scheduledRetryReason, previousRetryAgentId: cancelled.agentId, currentAssigneeAgentId: issue.assigneeAgentId, @@ -18535,10 +21836,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) let activeExecutionRun = issue.executionRunId ? await tx - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, issue.executionRunId)) - .then((rows) => rows[0] ?? null) + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.id, issue.executionRunId)) + .then((rows) => rows[0] ?? null) : null; if ( @@ -18550,7 +21851,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) activeExecutionRun = null; } - if (activeExecutionRun && await cancelStaleScheduledRetry(activeExecutionRun)) { + if ( + activeExecutionRun && + (await cancelStaleScheduledRetry(activeExecutionRun)) + ) { activeExecutionRun = null; } @@ -18580,7 +21884,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .set({ status: "cancelled", finishedAt: new Date(), - error: "Execution lock released after issue reassigned to a different agent", + error: + "Execution lock released after issue reassigned to a different agent", errorCode: "lock_released_on_reassignment", updatedAt: new Date(), }) @@ -18598,10 +21903,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .set({ status: "cancelled", finishedAt: new Date(), - error: "Execution lock released after issue reassigned to a different agent", + error: + "Execution lock released after issue reassigned to a different agent", updatedAt: new Date(), }) - .where(eq(agentWakeupRequests.id, activeExecutionRun.wakeupRequestId)); + .where( + eq( + agentWakeupRequests.id, + activeExecutionRun.wakeupRequestId, + ), + ); } activeExecutionRun = null; } @@ -18626,7 +21937,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .where( and( eq(heartbeatRuns.companyId, issue.companyId), - inArray(heartbeatRuns.status, [...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES]), + inArray(heartbeatRuns.status, [ + ...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES, + ]), sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`, ), ) @@ -18651,7 +21964,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .update(issues) .set({ executionRunId: legacyRun.id, - executionAgentNameKey: normalizeAgentNameKey(legacyAgent?.name), + executionAgentNameKey: normalizeAgentNameKey( + legacyAgent?.name, + ), executionLockedAt: new Date(), updatedAt: new Date(), }) @@ -18660,11 +21975,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } - const dependencyReadiness = await issuesSvc.listDependencyReadiness( - issue.companyId, - [issue.id], - tx, - ).then((rows) => rows.get(issue.id) ?? null); + const dependencyReadiness = await issuesSvc + .listDependencyReadiness(issue.companyId, [issue.id], tx) + .then((rows) => rows.get(issue.id) ?? null); // Blocked descendants should stay idle until the final blocker resolves. // Human comment/mention wakes are the exception: they may run in a @@ -18676,17 +21989,25 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (blockedInteractionWake) { enrichedContextSnapshot.dependencyBlockedInteraction = true; - enrichedContextSnapshot.unresolvedBlockerIssueIds = dependencyReadiness.unresolvedBlockerIssueIds; - enrichedContextSnapshot.unresolvedBlockerCount = dependencyReadiness.unresolvedBlockerCount; - enrichedContextSnapshot.unresolvedBlockerSummaries = await listUnresolvedBlockerSummaries( - tx, - issue.companyId, - issue.id, - dependencyReadiness.unresolvedBlockerIssueIds, - ); + enrichedContextSnapshot.unresolvedBlockerIssueIds = + dependencyReadiness.unresolvedBlockerIssueIds; + enrichedContextSnapshot.unresolvedBlockerCount = + dependencyReadiness.unresolvedBlockerCount; + enrichedContextSnapshot.unresolvedBlockerSummaries = + await listUnresolvedBlockerSummaries( + tx, + issue.companyId, + issue.id, + dependencyReadiness.unresolvedBlockerIssueIds, + ); } - if (!activeExecutionRun && dependencyReadiness && !dependencyReadiness.isDependencyReady && !blockedInteractionWake) { + if ( + !activeExecutionRun && + dependencyReadiness && + !dependencyReadiness.isDependencyReady && + !blockedInteractionWake + ) { await tx.insert(agentWakeupRequests).values({ companyId: agent.companyId, agentId, @@ -18696,7 +22017,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) payload: { ...(payload ?? {}), issueId, - unresolvedBlockerIssueIds: dependencyReadiness.unresolvedBlockerIssueIds, + unresolvedBlockerIssueIds: + dependencyReadiness.unresolvedBlockerIssueIds, }, status: "skipped", requestedByActorType: opts.requestedByActorType ?? null, @@ -18707,8 +22029,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { kind: "skipped" as const }; } - if (isolatedWorkspacesEnabled && !activeExecutionRun && issue.status !== "done" && issue.status !== "cancelled") { - const issueSettings = parseIssueExecutionWorkspaceSettings(issue.executionWorkspaceSettings); + if ( + isolatedWorkspacesEnabled && + !activeExecutionRun && + issue.status !== "done" && + issue.status !== "cancelled" + ) { + const issueSettings = parseIssueExecutionWorkspaceSettings( + issue.executionWorkspaceSettings, + ); const resolvedMode = resolveExecutionWorkspaceMode({ projectPolicy: null, issueSettings, @@ -18721,23 +22050,30 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) mode: resolvedMode, legacyUseProjectWorkspace: null, }); - const resolvedStrategy = resolveEffectiveWorkspaceStrategyType(resolvedMode, workspaceManagedConfig); + const resolvedStrategy = resolveEffectiveWorkspaceStrategyType( + resolvedMode, + workspaceManagedConfig, + ); const existingExecutionWorkspaceStatus = issue.executionWorkspaceId ? await tx - .select({ status: executionWorkspaces.status }) - .from(executionWorkspaces) - .where(and( - eq(executionWorkspaces.id, issue.executionWorkspaceId), - eq(executionWorkspaces.companyId, issue.companyId), - )) - .then((rows) => rows[0]?.status ?? null) + .select({ status: executionWorkspaces.status }) + .from(executionWorkspaces) + .where( + and( + eq(executionWorkspaces.id, issue.executionWorkspaceId), + eq(executionWorkspaces.companyId, issue.companyId), + ), + ) + .then((rows) => rows[0]?.status ?? null) : null; const reuseRequest = resolveExecutionWorkspaceReuseRequestForIssue({ issueExecutionWorkspaceId: issue.executionWorkspaceId, - issueExecutionWorkspacePreference: issue.executionWorkspacePreference, + issueExecutionWorkspacePreference: + issue.executionWorkspacePreference, existingExecutionWorkspaceStatus, }); - const hasResolvablePriorSessionWorkspace = await resolveHasResolvablePriorSessionWorkspace(); + const hasResolvablePriorSessionWorkspace = + await resolveHasResolvablePriorSessionWorkspace(); if ( isUnrunnableWorktreeCombo({ @@ -18745,16 +22081,21 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) projectId: issue.projectId ?? projectId ?? null, projectWorkspaceId: issue.projectWorkspaceId, executionWorkspaceId: issue.executionWorkspaceId, - executionWorkspacePreference: issue.executionWorkspacePreference, + executionWorkspacePreference: + issue.executionWorkspacePreference, }, resolvedMode, resolvedStrategy, - reusableExecutionWorkspaceAvailable: reuseRequest.existingExecutionWorkspaceAvailable, + reusableExecutionWorkspaceAvailable: + reuseRequest.existingExecutionWorkspaceAvailable, hasResolvablePriorSessionWorkspace, }) ) { const now = new Date(); - const issueLabel = formatIssueIdentifierLink(issue.identifier, issue.id); + const issueLabel = formatIssueIdentifierLink( + issue.identifier, + issue.id, + ); const blockedComment = [ `Paperclip blocked ${issueLabel} before dispatch because its workspace settings are not runnable.`, "", @@ -18836,15 +22177,20 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) normalizeAgentNameKey(issue.executionAgentNameKey) ?? normalizeAgentNameKey(executionAgent?.name); const isSameExecutionAgent = - Boolean(executionAgentNameKey) && executionAgentNameKey === agentNameKey; + Boolean(executionAgentNameKey) && + executionAgentNameKey === agentNameKey; const shouldDeferFollowupWake = shouldDeferFollowupWakeForSameIssue({ activeRunStatus: activeExecutionRun.status, isSameExecutionAgent, wakeCommentId, - forceFreshSession: enrichedContextSnapshot.forceFreshSession === true, + forceFreshSession: + enrichedContextSnapshot.forceFreshSession === true, }); const shouldQueueFollowupForRunningWake = - shouldQueueFollowupForRunningIssueWake({ contextSnapshot: enrichedContextSnapshot, wakeCommentId }) && + shouldQueueFollowupForRunningIssueWake({ + contextSnapshot: enrichedContextSnapshot, + wakeCommentId, + }) && activeExecutionRun.status === "running" && isSameExecutionAgent; const availableActiveExecutionRun = isSameExecutionAgent @@ -18852,10 +22198,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) : activeExecutionRun; if ( - isSameExecutionAgent - && !shouldDeferFollowupWake - && !shouldQueueFollowupForRunningWake - && availableActiveExecutionRun + isSameExecutionAgent && + !shouldDeferFollowupWake && + !shouldQueueFollowupForRunningWake && + availableActiveExecutionRun ) { const mergedContextSnapshot = mergeCoalescedContextSnapshot( availableActiveExecutionRun.contextSnapshot, @@ -18913,8 +22259,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .then((rows) => rows[0] ?? null); if (existingDeferred) { - const existingDeferredPayload = parseObject(existingDeferred.payload); - const existingDeferredContext = parseObject(existingDeferredPayload[DEFERRED_WAKE_CONTEXT_KEY]); + const existingDeferredPayload = parseObject( + existingDeferred.payload, + ); + const existingDeferredContext = parseObject( + existingDeferredPayload[DEFERRED_WAKE_CONTEXT_KEY], + ); const mergedDeferredContext = mergeCoalescedContextSnapshot( existingDeferredContext, enrichedContextSnapshot, @@ -18967,7 +22317,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) reason, wakeCommentId: wakeCommentId ?? null, requestedByActorType: opts.requestedByActorType ?? null, - forceFreshSession: enrichedContextSnapshot.forceFreshSession === true, + forceFreshSession: + enrichedContextSnapshot.forceFreshSession === true, hasExplicitResume: Boolean(explicitResumeSession), }) ) { @@ -18984,7 +22335,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eq(heartbeatRuns.companyId, agent.companyId), eq(heartbeatRuns.agentId, agentId), sql`${heartbeatRuns.finishedAt} is not null`, - gte(heartbeatRuns.finishedAt, new Date(throttleNow.getTime() - ISSUE_REWAKE_LOOKBACK_MS)), + gte( + heartbeatRuns.finishedAt, + new Date(throttleNow.getTime() - ISSUE_REWAKE_LOOKBACK_MS), + ), sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`, ), ) @@ -18992,7 +22346,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .limit(ISSUE_REWAKE_RUN_SAMPLE_LIMIT); if (recentTerminalRuns.length > 0) { - const sampleRunIds = recentTerminalRuns.map((sampleRun) => sampleRun.id); + const sampleRunIds = recentTerminalRuns.map( + (sampleRun) => sampleRun.id, + ); const progressRows = await tx .select({ runId: activityLog.runId }) .from(activityLog) @@ -19008,21 +22364,24 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const lastRunFinishedAt = recentTerminalRuns[0]?.finishedAt ?? null; const newInputRows = lastRunFinishedAt ? await tx - .select({ id: activityLog.id }) - .from(activityLog) - .where( - and( - eq(activityLog.companyId, agent.companyId), - eq(activityLog.entityType, "issue"), - eq(activityLog.entityId, issue.id), - gt(activityLog.createdAt, lastRunFinishedAt), - inArray(activityLog.action, ISSUE_NEW_INPUT_ACTIVITY_ACTIONS), - wakeCommentId && opts.requestedByActorType === "agent" - ? ne(activityLog.actorType, "agent") - : undefined, - ), - ) - .limit(1) + .select({ id: activityLog.id }) + .from(activityLog) + .where( + and( + eq(activityLog.companyId, agent.companyId), + eq(activityLog.entityType, "issue"), + eq(activityLog.entityId, issue.id), + gt(activityLog.createdAt, lastRunFinishedAt), + inArray( + activityLog.action, + ISSUE_NEW_INPUT_ACTIVITY_ACTIONS, + ), + wakeCommentId && opts.requestedByActorType === "agent" + ? ne(activityLog.actorType, "agent") + : undefined, + ), + ) + .limit(1) : []; const throttleDecision = evaluateIssueRewakeThrottle({ @@ -19055,7 +22414,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) requestedReason: reason, noProgressStreak: throttleDecision.noProgressStreak, cooldownMs: throttleDecision.cooldownMs, - lastRunFinishedAt: throttleDecision.lastRunFinishedAt.toISOString(), + lastRunFinishedAt: + throttleDecision.lastRunFinishedAt.toISOString(), nextAllowedAt: throttleDecision.nextAllowedAt.toISOString(), }, }, @@ -19070,7 +22430,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } - const dailyCapBlock = await getHeartbeatDailyCapBlock(agent, policy, {}, tx); + const dailyCapBlock = await getHeartbeatDailyCapBlock( + agent, + policy, + {}, + tx, + ); if (dailyCapBlock) { const now = new Date(); await tx.insert(agentWakeupRequests).values({ @@ -19082,7 +22447,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) payload: { ...(payload ?? {}), heartbeatSkip: { - reason: "Per-agent heartbeat daily cap reached before adapter invocation.", + reason: + "Per-agent heartbeat daily cap reached before adapter invocation.", observed: dailyCapBlock.observed, limit: dailyCapBlock.limit, }, @@ -19154,7 +22520,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { kind: "queued" as const, run: newRun }; }); - if (outcome.kind === "deferred" || outcome.kind === "skipped") return null; + if (outcome.kind === "deferred" || outcome.kind === "skipped") + return null; if (outcome.kind === "coalesced") { await startNextQueuedRunForAgent(agent.id); return outcome.run; @@ -19180,26 +22547,44 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const activeRuns = await db .select() .from(heartbeatRuns) - .where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, [...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES]))) + .where( + and( + eq(heartbeatRuns.agentId, agentId), + inArray(heartbeatRuns.status, [ + ...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES, + ]), + ), + ) .orderBy(desc(heartbeatRuns.createdAt)); const sameScopeQueuedRun = activeRuns.find( - (candidate) => candidate.status === "queued" && isSameTaskScope(runTaskKey(candidate), taskKey), + (candidate) => + candidate.status === "queued" && + isSameTaskScope(runTaskKey(candidate), taskKey), ); const sameScopeScheduledRetryRun = activeRuns.find( - (candidate) => candidate.status === "scheduled_retry" && isSameTaskScope(runTaskKey(candidate), taskKey), + (candidate) => + candidate.status === "scheduled_retry" && + isSameTaskScope(runTaskKey(candidate), taskKey), ); const sameScopeRunningRun = activeRuns.find( - (candidate) => candidate.status === "running" && isSameTaskScope(runTaskKey(candidate), taskKey), + (candidate) => + candidate.status === "running" && + isSameTaskScope(runTaskKey(candidate), taskKey), ); const shouldQueueFollowupForRunningWake = Boolean(sameScopeRunningRun) && !sameScopeQueuedRun && - shouldQueueFollowupForRunningIssueWake({ contextSnapshot: enrichedContextSnapshot, wakeCommentId }); + shouldQueueFollowupForRunningIssueWake({ + contextSnapshot: enrichedContextSnapshot, + wakeCommentId, + }); const rawCoalescedTarget = sameScopeQueuedRun ?? sameScopeScheduledRetryRun ?? - (shouldQueueFollowupForRunningWake ? null : sameScopeRunningRun ?? null); + (shouldQueueFollowupForRunningWake + ? null + : (sameScopeRunningRun ?? null)); const coalescedTargetRun = filterZombieCoalesceTarget( rawCoalescedTarget, @@ -19244,7 +22629,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) sql`select id from agents where id = ${agentId} and company_id = ${agent.companyId} for update`, ); - const dailyCapBlock = await getHeartbeatDailyCapBlock(agent, policy, {}, tx); + const dailyCapBlock = await getHeartbeatDailyCapBlock( + agent, + policy, + {}, + tx, + ); if (dailyCapBlock) { const now = new Date(); await tx.insert(agentWakeupRequests).values({ @@ -19256,7 +22646,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) payload: { ...(payload ?? {}), heartbeatSkip: { - reason: "Per-agent heartbeat daily cap reached before adapter invocation.", + reason: + "Per-agent heartbeat daily cap reached before adapter invocation.", observed: dailyCapBlock.observed, limit: dailyCapBlock.limit, }, @@ -19345,8 +22736,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } async function listProjectScopedRunIds(companyId: string, projectId: string) { - const runIssueId = sql`${heartbeatRuns.contextSnapshot} ->> 'issueId'`; - const effectiveProjectId = sql`coalesce(${heartbeatRuns.contextSnapshot} ->> 'projectId', ${issues.projectId}::text)`; + const runIssueId = sql< + string | null + >`${heartbeatRuns.contextSnapshot} ->> 'issueId'`; + const effectiveProjectId = sql< + string | null + >`coalesce(${heartbeatRuns.contextSnapshot} ->> 'projectId', ${issues.projectId}::text)`; const rows = await db .selectDistinctOn([heartbeatRuns.id], { id: heartbeatRuns.id }) @@ -19361,7 +22756,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .where( and( eq(heartbeatRuns.companyId, companyId), - inArray(heartbeatRuns.status, [...CANCELLABLE_HEARTBEAT_RUN_STATUSES]), + inArray(heartbeatRuns.status, [ + ...CANCELLABLE_HEARTBEAT_RUN_STATUSES, + ]), sql`${effectiveProjectId} = ${projectId}`, ), ); @@ -19369,12 +22766,21 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return rows.map((row) => row.id); } - async function listProjectScopedWakeupIds(companyId: string, projectId: string) { - const wakeIssueId = sql`${agentWakeupRequests.payload} ->> 'issueId'`; - const effectiveProjectId = sql`coalesce(${agentWakeupRequests.payload} ->> 'projectId', ${issues.projectId}::text)`; + async function listProjectScopedWakeupIds( + companyId: string, + projectId: string, + ) { + const wakeIssueId = sql< + string | null + >`${agentWakeupRequests.payload} ->> 'issueId'`; + const effectiveProjectId = sql< + string | null + >`coalesce(${agentWakeupRequests.payload} ->> 'projectId', ${issues.projectId}::text)`; const rows = await db - .selectDistinctOn([agentWakeupRequests.id], { id: agentWakeupRequests.id }) + .selectDistinctOn([agentWakeupRequests.id], { + id: agentWakeupRequests.id, + }) .from(agentWakeupRequests) .leftJoin( issues, @@ -19386,7 +22792,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .where( and( eq(agentWakeupRequests.companyId, companyId), - inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), + inArray(agentWakeupRequests.status, [ + "queued", + "deferred_issue_execution", + ]), sql`${agentWakeupRequests.runId} is null`, sql`${effectiveProjectId} = ${projectId}`, ), @@ -19395,7 +22804,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return rows.map((row) => row.id); } - async function cancelPendingWakeupsForBudgetScope(scope: BudgetEnforcementScope) { + async function cancelPendingWakeupsForBudgetScope( + scope: BudgetEnforcementScope, + ) { const now = new Date(); let wakeupIds: string[] = []; @@ -19406,7 +22817,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .where( and( eq(agentWakeupRequests.companyId, scope.companyId), - inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), + inArray(agentWakeupRequests.status, [ + "queued", + "deferred_issue_execution", + ]), sql`${agentWakeupRequests.runId} is null`, ), ) @@ -19419,13 +22833,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) and( eq(agentWakeupRequests.companyId, scope.companyId), eq(agentWakeupRequests.agentId, scope.scopeId), - inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), + inArray(agentWakeupRequests.status, [ + "queued", + "deferred_issue_execution", + ]), sql`${agentWakeupRequests.runId} is null`, ), ) .then((rows) => rows.map((row) => row.id)); } else { - wakeupIds = await listProjectScopedWakeupIds(scope.companyId, scope.scopeId); + wakeupIds = await listProjectScopedWakeupIds( + scope.companyId, + scope.scopeId, + ); } if (wakeupIds.length === 0) return 0; @@ -19450,10 +22870,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) eventPayload?: Record; }; - async function cancelRunInternal(runId: string, reason = "Cancelled by control plane", options: CancelRunOptions = {}) { + async function cancelRunInternal( + runId: string, + reason = "Cancelled by control plane", + options: CancelRunOptions = {}, + ) { const run = await getRun(runId); if (!run) throw notFound("Heartbeat run not found"); - if (!CANCELLABLE_HEARTBEAT_RUN_STATUSES.includes(run.status as (typeof CANCELLABLE_HEARTBEAT_RUN_STATUSES)[number])) return run; + if ( + !CANCELLABLE_HEARTBEAT_RUN_STATUSES.includes( + run.status as (typeof CANCELLABLE_HEARTBEAT_RUN_STATUSES)[number], + ) + ) + return run; const agent = await getAgent(run.agentId); const errorCode = options.errorCode ?? "cancelled"; const resultJson = agent @@ -19469,8 +22898,14 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const running = runningProcesses.get(run.id); try { - const nativeCancellation = await cancelNativeSession(run.id, reason, { db, scope: "run" }); - if (run.runtimeMode === "native" && (!nativeCancellation.decision || !nativeCancellation.auditId)) { + const nativeCancellation = await cancelNativeSession(run.id, reason, { + db, + scope: "run", + }); + if ( + run.runtimeMode === "native" && + (!nativeCancellation.decision || !nativeCancellation.auditId) + ) { throw new Error("native_cancellation_outcome_not_audited"); } if (running) { @@ -19490,15 +22925,23 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const finishedAt = new Date(); - const persistedCancellationResult = run.runtimeMode === "native" - ? await getRun(run.id).then((current) => parseObject(current?.resultJson)) - : {}; + const persistedCancellationResult = + run.runtimeMode === "native" + ? await getRun(run.id).then((current) => + parseObject(current?.resultJson), + ) + : {}; const cancelled = await setRunStatus(run.id, "cancelled", { finishedAt, error: reason, errorCode, ...(resultJson || Object.keys(persistedCancellationResult).length > 0 - ? { resultJson: { ...persistedCancellationResult, ...(resultJson ?? {}) } } + ? { + resultJson: { + ...persistedCancellationResult, + ...(resultJson ?? {}), + }, + } : {}), }); @@ -19525,25 +22968,38 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return cancelled; } - async function cancelActiveForAgentInternal(agentId: string, reason = "Cancelled due to agent pause", errorCode = "cancelled") { + async function cancelActiveForAgentInternal( + agentId: string, + reason = "Cancelled due to agent pause", + errorCode = "cancelled", + ) { const agent = await getAgent(agentId); const runs = await db .select() .from(heartbeatRuns) - .where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, [...CANCELLABLE_HEARTBEAT_RUN_STATUSES]))); + .where( + and( + eq(heartbeatRuns.agentId, agentId), + inArray(heartbeatRuns.status, [ + ...CANCELLABLE_HEARTBEAT_RUN_STATUSES, + ]), + ), + ); for (const run of runs) { await setRunStatus(run.id, "cancelled", { finishedAt: new Date(), error: reason, errorCode, - ...(agent ? { - resultJson: mergeRunStopMetadataForAgent(agent, "cancelled", { - resultJson: parseObject(run.resultJson), - errorCode, - errorMessage: reason, - }), - } : {}), + ...(agent + ? { + resultJson: mergeRunStopMetadataForAgent(agent, "cancelled", { + resultJson: parseObject(run.resultJson), + errorCode, + errorMessage: reason, + }), + } + : {}), }); await setWakeupStatus(run.wakeupRequestId, "cancelled", { @@ -19571,8 +23027,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return runs.length; } - async function cancelPendingWakeupsForAgentsInternal(agentIds: string[], reason: string) { - const uniqueAgentIds = [...new Set(agentIds)].filter((agentId) => agentId.length > 0); + async function cancelPendingWakeupsForAgentsInternal( + agentIds: string[], + reason: string, + ) { + const uniqueAgentIds = [...new Set(agentIds)].filter( + (agentId) => agentId.length > 0, + ); if (uniqueAgentIds.length === 0) return 0; const now = new Date(); @@ -19582,7 +23043,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .where( and( inArray(agentWakeupRequests.agentId, uniqueAgentIds), - inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), + inArray(agentWakeupRequests.status, [ + "queued", + "deferred_issue_execution", + ]), sql`${agentWakeupRequests.runId} is null`, ), ) @@ -19603,13 +23067,21 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return wakeupIds.length; } - async function cancelInvocationsForAgentsInternal(agentIds: string[], reason: string) { - const uniqueAgentIds = [...new Set(agentIds)].filter((agentId) => agentId.length > 0); + async function cancelInvocationsForAgentsInternal( + agentIds: string[], + reason: string, + ) { + const uniqueAgentIds = [...new Set(agentIds)].filter( + (agentId) => agentId.length > 0, + ); let runsCancelled = 0; for (const agentId of uniqueAgentIds) { runsCancelled += await cancelActiveForAgentInternal(agentId, reason); } - const wakeupsCancelled = await cancelPendingWakeupsForAgentsInternal(uniqueAgentIds, reason); + const wakeupsCancelled = await cancelPendingWakeupsForAgentsInternal( + uniqueAgentIds, + reason, + ); return { agentIds: uniqueAgentIds, runsCancelled, @@ -19619,7 +23091,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) async function cancelBudgetScopeWork(scope: BudgetEnforcementScope) { if (scope.scopeType === "agent") { - await cancelActiveForAgentInternal(scope.scopeId, "Cancelled due to budget pause"); + await cancelActiveForAgentInternal( + scope.scopeId, + "Cancelled due to budget pause", + ); await cancelPendingWakeupsForBudgetScope(scope); return; } @@ -19627,15 +23102,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const runIds = scope.scopeType === "company" ? await db - .select({ id: heartbeatRuns.id }) - .from(heartbeatRuns) - .where( - and( - eq(heartbeatRuns.companyId, scope.companyId), - inArray(heartbeatRuns.status, [...CANCELLABLE_HEARTBEAT_RUN_STATUSES]), - ), - ) - .then((rows) => rows.map((row) => row.id)) + .select({ id: heartbeatRuns.id }) + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.companyId, scope.companyId), + inArray(heartbeatRuns.status, [ + ...CANCELLABLE_HEARTBEAT_RUN_STATUSES, + ]), + ), + ) + .then((rows) => rows.map((row) => row.id)) : await listProjectScopedRunIds(scope.companyId, scope.scopeId); for (const runId of runIds) { @@ -19656,7 +23133,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) while (liveRunExecutions.has(runId)) { if (Date.now() >= deadline) { - throw new Error(`Timed out waiting for heartbeat run ${runId} execution to drain`); + throw new Error( + `Timed out waiting for heartbeat run ${runId} execution to drain`, + ); } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } @@ -19677,21 +23156,24 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ...heartbeatRunListContextColumns, } : safeForLegacyEncoding - ? { - ...heartbeatRunListColumns, - error: sql`NULL`.as("error"), - ...heartbeatRunListContextColumns, - } - : { - ...heartbeatRunListColumns, - ...heartbeatRunListContextColumns, - ...heartbeatRunListResultColumns, - }, + ? { + ...heartbeatRunListColumns, + error: sql`NULL`.as("error"), + ...heartbeatRunListContextColumns, + } + : { + ...heartbeatRunListColumns, + ...heartbeatRunListContextColumns, + ...heartbeatRunListResultColumns, + }, ) .from(heartbeatRuns) .where( agentId - ? and(eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.agentId, agentId)) + ? and( + eq(heartbeatRuns.companyId, companyId), + eq(heartbeatRuns.agentId, agentId), + ) : eq(heartbeatRuns.companyId, companyId), ) .orderBy(desc(heartbeatRuns.createdAt)); @@ -19737,17 +23219,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) wakeSource: contextWakeSource, wakeTriggerDetail: contextWakeTriggerDetail, }), - resultJson: safeForLegacyEncoding || summary - ? null - : summarizeHeartbeatRunListResultJson({ - summary: resultSummary, - result: resultResult, - message: resultMessage, - error: resultError, - totalCostUsd: resultTotalCostUsd, - costUsd: resultCostUsd, - costUsdCamel: resultCostUsdCamel, - }), + resultJson: + safeForLegacyEncoding || summary + ? null + : summarizeHeartbeatRunListResultJson({ + summary: resultSummary, + result: resultResult, + message: resultMessage, + error: resultError, + totalCostUsd: resultTotalCostUsd, + costUsd: resultCostUsd, + costUsdCamel: resultCostUsdCamel, + }), }; }); }, @@ -19768,13 +23251,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const latestTaskSession = await db .select() .from(agentTaskSessions) - .where(and(eq(agentTaskSessions.companyId, agent.companyId), eq(agentTaskSessions.agentId, agent.id))) + .where( + and( + eq(agentTaskSessions.companyId, agent.companyId), + eq(agentTaskSessions.agentId, agent.id), + ), + ) .orderBy(desc(agentTaskSessions.updatedAt)) .limit(1) .then((rows) => rows[0] ?? null); return { ...ensured, - sessionDisplayId: latestTaskSession?.sessionDisplayId ?? ensured.sessionId, + sessionDisplayId: + latestTaskSession?.sessionDisplayId ?? ensured.sessionId, sessionParamsJson: latestTaskSession?.sessionParamsJson ?? null, }; }, @@ -19786,11 +23275,22 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return db .select() .from(agentTaskSessions) - .where(and(eq(agentTaskSessions.companyId, agent.companyId), eq(agentTaskSessions.agentId, agentId))) - .orderBy(desc(agentTaskSessions.updatedAt), desc(agentTaskSessions.createdAt)); + .where( + and( + eq(agentTaskSessions.companyId, agent.companyId), + eq(agentTaskSessions.agentId, agentId), + ), + ) + .orderBy( + desc(agentTaskSessions.updatedAt), + desc(agentTaskSessions.createdAt), + ); }, - resetRuntimeSession: async (agentId: string, opts?: { taskKey?: string | null }) => { + resetRuntimeSession: async ( + agentId: string, + opts?: { taskKey?: string | null }, + ) => { const agent = await getAgent(agentId); if (!agent) throw notFound("Agent not found"); await ensureRuntimeState(agent); @@ -19829,7 +23329,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) db .select() .from(heartbeatRunEvents) - .where(and(eq(heartbeatRunEvents.runId, runId), gt(heartbeatRunEvents.seq, afterSeq))) + .where( + and( + eq(heartbeatRunEvents.runId, runId), + gt(heartbeatRunEvents.seq, afterSeq), + ), + ) .orderBy(asc(heartbeatRunEvents.seq)) .limit(Math.max(1, Math.min(limit, 1000))), @@ -19853,16 +23358,22 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }, readLog: async ( - runOrLookup: string | { - id: string; - companyId: string; - logStore: string | null; - logRef: string | null; - }, + runOrLookup: + | string + | { + id: string; + companyId: string; + logStore: string | null; + logRef: string | null; + }, opts?: { offset?: number; limitBytes?: number }, ) => { - const run = typeof runOrLookup === "string" ? await getRunLogAccess(runOrLookup) : runOrLookup; - const runId = typeof runOrLookup === "string" ? runOrLookup : runOrLookup.id; + const run = + typeof runOrLookup === "string" + ? await getRunLogAccess(runOrLookup) + : runOrLookup; + const runId = + typeof runOrLookup === "string" ? runOrLookup : runOrLookup.id; if (!run) throw notFound("Heartbeat run not found"); if (!run.logStore || !run.logRef) throw notFound("Run log not found"); @@ -19890,7 +23401,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "timer" | "assignment" | "on_demand" | "automation" = "on_demand", contextSnapshot: Record = {}, triggerDetail: "manual" | "ping" | "callback" | "system" = "manual", - actor?: { actorType?: "user" | "agent" | "system"; actorId?: string | null }, + actor?: { + actorType?: "user" | "agent" | "system"; + actorId?: string | null; + }, ) => trackWakeup(agentId, { source, @@ -19974,13 +23488,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .from(agents) .innerJoin(companies, eq(companies.id, agents.companyId)) .where(eq(companies.status, "active")); - const agentsByCompany = groupAgentOrgRowsByCompany(allAgents.map(toAgentOrgRow)); + const agentsByCompany = groupAgentOrgRowsByCompany( + allAgents.map(toAgentOrgRow), + ); let checked = 0; let enqueued = 0; let skipped = 0; for (const agent of allAgents) { - const invokability = evaluateAgentInvokability(toAgentOrgRow(agent), agentsByCompany.get(agent.companyId) ?? []); + const invokability = evaluateAgentInvokability( + toAgentOrgRow(agent), + agentsByCompany.get(agent.companyId) ?? [], + ); if (!invokability.invokable) continue; const policy = parseHeartbeatPolicy(agent); if (!policy.enabled || policy.intervalSec <= 0) continue; @@ -19989,22 +23508,30 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const eligibleIssue = await db .select({ id: issues.id }) .from(issues) - .where(and( - eq(issues.companyId, agent.companyId), - eq(issues.assigneeAgentId, agent.id), - inArray(issues.status, ["todo", "in_progress"]), - gte(issues.createdAt, cutoff), - )) + .where( + and( + eq(issues.companyId, agent.companyId), + eq(issues.assigneeAgentId, agent.id), + inArray(issues.status, ["todo", "in_progress"]), + gte(issues.createdAt, cutoff), + ), + ) .limit(1) .then((rows) => rows[0] ?? null); if (!eligibleIssue) continue; } checked += 1; - const baseline = new Date(agent.lastHeartbeatAt ?? agent.createdAt).getTime(); + const baseline = new Date( + agent.lastHeartbeatAt ?? agent.createdAt, + ).getTime(); const elapsedMs = now.getTime() - baseline; if (elapsedMs < policy.intervalSec * 1000) continue; - const timerClaim = await claimDueTimerHeartbeat(agent, now, policy.intervalSec); + const timerClaim = await claimDueTimerHeartbeat( + agent, + now, + policy.intervalSec, + ); if (!timerClaim) continue; const run = await enqueueWakeup(agent.id, { @@ -20033,14 +23560,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }; }, - cancelRun: (runId: string, reason?: string, options?: CancelRunOptions) => cancelRunInternal(runId, reason, options), + cancelRun: (runId: string, reason?: string, options?: CancelRunOptions) => + cancelRunInternal(runId, reason, options), /** * Pause-only. Emits errorCode "agent_paused" unconditionally; its sole caller is the * agent pause route. For non-pause cancellations use cancelRun, or call the internal * cancelActiveForAgentInternal(agentId, reason, errorCode) with an explicit errorCode. */ - cancelActiveForAgent: (agentId: string, reason?: string) => cancelActiveForAgentInternal(agentId, reason, "agent_paused"), + cancelActiveForAgent: (agentId: string, reason?: string) => + cancelActiveForAgentInternal(agentId, reason, "agent_paused"), cancelInvocationsForAgents: (agentIds: string[], reason: string) => cancelInvocationsForAgentsInternal(agentIds, reason), diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index f10a23aa86f..01dc9745de5 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -95,6 +95,10 @@ import { resolveIssueGoalId, resolveNextIssueGoalId } from "./issue-goal-fallbac import { getRunLogStore } from "./run-log-store.js"; import { getDefaultCompanyGoal } from "./goals.js"; import { assertAssignableAgent } from "./agent-assignability.js"; +import { + LEGACY_WITHHELD_RUN_COMMENT, + projectHistoricalHeartbeatRunComment, +} from "./heartbeat-run-summary.js"; import { DEFAULT_INSERT_CHUNK_ROWS, insertRowsInChunks } from "./batch-insert.js"; import type { ImportIssueRow, @@ -4427,6 +4431,37 @@ export function issueService(db: Db) { return enriched; } + async function projectHistoricalRunComments< + T extends { body: string; createdByRunId: string | null }, + >(comments: T[]): Promise { + const runIds = [ + ...new Set( + comments.flatMap((comment) => + comment.createdByRunId && + comment.body === LEGACY_WITHHELD_RUN_COMMENT + ? [comment.createdByRunId] + : [], + ), + ), + ]; + if (runIds.length === 0) return comments; + const runResults = await db + .select({ id: heartbeatRuns.id, resultJson: heartbeatRuns.resultJson }) + .from(heartbeatRuns) + .where(inArray(heartbeatRuns.id, runIds)); + const resultByRunId = new Map( + runResults.map((run) => [run.id, parseObject(run.resultJson)]), + ); + return comments.map((comment) => { + if (!comment.createdByRunId) return comment; + const body = projectHistoricalHeartbeatRunComment( + comment.body, + resultByRunId.get(comment.createdByRunId), + ); + return body === comment.body ? comment : { ...comment, body }; + }); + } + async function getCurrentScheduledRetryForIssue(issueId: string, companyId: string): Promise { const row = await db .select({ @@ -8578,7 +8613,8 @@ export function issueService(db: Db) { const comments = limit ? await query.limit(limit) : await query; const { censorUsernameInLogs } = await instanceSettings.getGeneral(); - const enrichedComments = await enrichCommentsWithDerivedAgentAttribution(comments); + const projectedComments = await projectHistoricalRunComments(comments); + const enrichedComments = await enrichCommentsWithDerivedAgentAttribution(projectedComments); return enrichedComments.map((comment) => redactIssueComment(comment, censorUsernameInLogs)); }, @@ -8618,8 +8654,14 @@ export function issueService(db: Db) { .where(eq(issueComments.id, commentId)) .then((rows) => rows[0] ?? null); if (!comment) return null; - const [enrichedComment] = await enrichCommentsWithDerivedAgentAttribution([comment]); - return redactIssueComment(enrichedComment ?? comment, censorUsernameInLogs); + const [projectedComment] = await projectHistoricalRunComments([comment]); + const [enrichedComment] = await enrichCommentsWithDerivedAgentAttribution([ + projectedComment ?? comment, + ]); + return redactIssueComment( + enrichedComment ?? projectedComment ?? comment, + censorUsernameInLogs, + ); }, removeComment: async (commentId: string) => { diff --git a/server/src/services/provider-trace-store.ts b/server/src/services/provider-trace-store.ts new file mode 100644 index 00000000000..d0a12c5c2ca --- /dev/null +++ b/server/src/services/provider-trace-store.ts @@ -0,0 +1,497 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { createHash, randomUUID } from "node:crypto"; +import { and, eq, inArray, isNull, lt } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { providerTraceRecords } from "@paperclipai/db"; +import type { + ProviderTraceFrame, + ProviderTraceMetadata, +} from "@paperclipai/shared"; +import { resolvePaperclipInstanceRoot } from "../home-paths.js"; +import { logActivity } from "./activity-log.js"; + +export const PROVIDER_TRACE_MAX_BYTES = 64 * 1024 * 1024; +export const PROVIDER_TRACE_RETENTION_MS = 24 * 60 * 60 * 1_000; + +type TraceRow = typeof providerTraceRecords.$inferSelect; + +function traceRoot() { + return ( + process.env.PROVIDER_TRACE_BASE_PATH ?? + path.resolve(resolvePaperclipInstanceRoot(), "data", "provider-traces") + ); +} + +function tracePath(traceRef: string) { + if ( + path.basename(traceRef) !== traceRef || + !/^[a-f0-9-]+\.ndjson$/.test(traceRef) + ) { + throw new Error("invalid_provider_trace_ref"); + } + return path.resolve(traceRoot(), traceRef); +} + +function rehydrationTracePath(traceRef: string) { + return `${tracePath(traceRef)}.rehydration`; +} + +function asMetadata(row: TraceRow): ProviderTraceMetadata { + return { + schema: "paperclip.provider_trace_metadata.v1", + id: row.id, + runId: row.runId, + companyId: row.companyId, + status: row.status as ProviderTraceMetadata["status"], + provider: row.provider, + frameCount: row.frameCount, + byteCount: row.byteCount, + digest: row.digest as ProviderTraceMetadata["digest"], + reason: row.reason, + requestedBy: row.requestedBy, + createdAt: row.createdAt, + expiresAt: row.expiresAt, + deletedAt: row.deletedAt, + }; +} + +function record(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +} + +const SECRET_KEY = + /(authorization|cookie|credential|password|passwd|secret|token|api[_-]?key|private[_-]?key)/i; +const REASONING_KEY = + /(^|[_-])(analysis|reasoning|chain[_-]?of[_-]?thought)($|[_-])/i; +const SECRET_VALUE = + /(?:\bBearer\s+[A-Za-z0-9._~+/=-]{8,}|\bsk-[A-Za-z0-9_-]{12,}|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----)/i; + +function redactParsed( + value: unknown, + pathParts: string[] = [], + withheld: string[] = [], +): unknown { + if (Array.isArray(value)) { + return value.map((item, index) => + redactParsed(item, [...pathParts, String(index)], withheld), + ); + } + if (typeof value === "string" && SECRET_VALUE.test(value)) { + withheld.push(pathParts.join(".") || "$raw"); + return "[withheld]"; + } + if (!value || typeof value !== "object") return value; + const source = value as Record; + const reasoningObject = + source.type === "reasoning" || source.type === "analysis"; + const output: Record = {}; + for (const [key, child] of Object.entries(source)) { + const childPath = [...pathParts, key]; + if ( + SECRET_KEY.test(key) || + REASONING_KEY.test(key) || + (reasoningObject && key !== "type" && key !== "id" && key !== "status") + ) { + output[key] = "[withheld]"; + withheld.push(childPath.join(".")); + continue; + } + output[key] = redactParsed(child, childPath, withheld); + } + return output; +} + +function redactedFrame(entry: Record) { + const rawBase64 = typeof entry.rawBase64 === "string" ? entry.rawBase64 : ""; + const withheldPaths: string[] = []; + let parsed: unknown = null; + try { + parsed = redactParsed( + JSON.parse(Buffer.from(rawBase64, "base64").toString("utf8")), + [], + withheldPaths, + ); + } catch { + withheldPaths.push("$raw"); + } + const { rawBase64: _raw, ...safe } = entry; + return { + ...safe, + parsed, + withheldPaths, + exactAvailable: rawBase64.length > 0, + }; +} + +export function redactProviderTraceFrame(entry: Record) { + return redactedFrame(entry); +} + +async function readTraceBytes(traceRef: string) { + const nativeBytes = await fs.readFile(tracePath(traceRef)); + const rehydrationBytes = await fs + .readFile(rehydrationTracePath(traceRef)) + .catch(() => Buffer.alloc(0)); + if (rehydrationBytes.byteLength === 0) return nativeBytes; + const separator = + nativeBytes.byteLength > 0 && nativeBytes.at(-1) !== 0x0a + ? Buffer.from("\n") + : Buffer.alloc(0); + return Buffer.concat([nativeBytes, separator, rehydrationBytes]); +} + +async function readEntries(traceRef: string) { + const content = (await readTraceBytes(traceRef)).toString("utf8"); + return content.split("\n").flatMap((line) => { + if (!line.trim()) return []; + try { + return [record(JSON.parse(line))]; + } catch { + return [ + { + kind: "trace_status", + status: "incomplete", + reason: "invalid_sidecar_record", + }, + ]; + } + }); +} + +export function assessProviderTraceEntries( + entries: Record[], + options: { + invalidRecordCount?: number; + requiredChannels?: string[]; + } = {}, +): { status: "complete" | "incomplete" | "truncated"; reason: string | null } { + if ((options.invalidRecordCount ?? 0) > 0) { + return { status: "incomplete", reason: "invalid_sidecar_record" }; + } + + for (const entry of entries) { + if (entry.kind !== "frame") continue; + const encoded = + typeof entry.rawBase64 === "string" ? entry.rawBase64 : null; + const byteLength = Number(entry.byteLength); + const digest = typeof entry.digest === "string" ? entry.digest : null; + if (encoded === null || !Number.isSafeInteger(byteLength) || !digest) { + return { status: "incomplete", reason: "invalid_frame_metadata" }; + } + const raw = Buffer.from(encoded, "base64"); + if ( + raw.toString("base64") !== encoded || + raw.byteLength !== byteLength || + `sha256:${createHash("sha256").update(raw).digest("hex")}` !== digest + ) { + return { status: "incomplete", reason: "provider_frame_digest_mismatch" }; + } + } + + const channels = new Set(options.requiredChannels ?? []); + for (const entry of entries) { + if (typeof entry.debugChannel === "string") + channels.add(entry.debugChannel); + } + for (const channel of channels) { + const channelEntries = entries.filter( + (entry) => entry.debugChannel === channel, + ); + if (channelEntries.length === 0) { + return { + status: "incomplete", + reason: `trace_channel_missing:${channel}`, + }; + } + for (let index = 0; index < channelEntries.length; index += 1) { + if (Number(channelEntries[index]?.debugSequence) !== index + 1) { + return { + status: "incomplete", + reason: `trace_debug_sequence_gap:${channel}`, + }; + } + } + const terminal = channelEntries.at(-1)!; + if (terminal.kind !== "trace_status") { + return { + status: "incomplete", + reason: `trace_channel_ack_missing:${channel}`, + }; + } + if ( + Number(terminal.acknowledgedDebugSequence) !== + Number(terminal.debugSequence) - 1 + ) { + return { + status: "incomplete", + reason: `trace_channel_ack_invalid:${channel}`, + }; + } + } + + const statuses = entries.filter((entry) => entry.kind === "trace_status"); + const incomplete = statuses.find((entry) => entry.status === "incomplete"); + if (incomplete) { + return { + status: "incomplete", + reason: + typeof incomplete.reason === "string" + ? incomplete.reason + : "trace_channel_incomplete", + }; + } + const truncated = statuses.find((entry) => entry.status === "truncated"); + if (truncated) { + return { + status: "truncated", + reason: + typeof truncated.reason === "string" + ? truncated.reason + : "provider_trace_max_bytes_exceeded", + }; + } + if (statuses.length === 0) { + return { status: "incomplete", reason: "trace_terminal_ack_missing" }; + } + return { status: "complete", reason: null }; +} + +export function providerTraceStore(db: Db) { + async function cleanupExpired(now = new Date()) { + const expired = await db + .select() + .from(providerTraceRecords) + .where( + and( + lt(providerTraceRecords.expiresAt, now), + isNull(providerTraceRecords.deletedAt), + ), + ); + for (const row of expired) { + await fs.unlink(tracePath(row.traceRef)).catch(() => undefined); + await fs + .unlink(rehydrationTracePath(row.traceRef)) + .catch(() => undefined); + await db + .update(providerTraceRecords) + .set({ + status: "expired", + deletedAt: now, + updatedAt: now, + }) + .where(eq(providerTraceRecords.id, row.id)); + await logActivity(db, { + companyId: row.companyId, + actorType: "system", + actorId: "provider-trace-expiry", + runId: row.runId, + action: "provider_trace.expired", + entityType: "heartbeat_run", + entityId: row.runId, + details: { + traceId: row.id, + expiredAt: now.toISOString(), + payloadLogged: false, + }, + }); + } + return expired; + } + + async function prepare(input: { + runId: string; + companyId: string; + provider: string; + requestedBy: string; + }) { + await cleanupExpired().catch(() => []); + const existing = await db + .select() + .from(providerTraceRecords) + .where(eq(providerTraceRecords.runId, input.runId)) + .limit(1) + .then((rows) => rows[0] ?? null); + if (existing) + return { + metadata: asMetadata(existing), + path: tracePath(existing.traceRef), + }; + + await fs.mkdir(traceRoot(), { recursive: true, mode: 0o700 }); + const traceRef = `${randomUUID()}.ndjson`; + const filePath = tracePath(traceRef); + const handle = await fs.open(filePath, "wx", 0o600); + await handle.close(); + await fs.chmod(filePath, 0o600); + const expiresAt = new Date(Date.now() + PROVIDER_TRACE_RETENTION_MS); + const row = await db + .insert(providerTraceRecords) + .values({ + runId: input.runId, + companyId: input.companyId, + status: "capturing", + provider: input.provider, + traceRef, + requestedBy: input.requestedBy, + expiresAt, + }) + .returning() + .then((rows) => rows[0]); + if (!row) throw new Error("provider_trace_metadata_create_failed"); + return { metadata: asMetadata(row), path: filePath }; + } + + async function getByRun(runId: string, companyId?: string) { + const where = companyId + ? and( + eq(providerTraceRecords.runId, runId), + eq(providerTraceRecords.companyId, companyId), + ) + : eq(providerTraceRecords.runId, runId); + return db + .select() + .from(providerTraceRecords) + .where(where) + .limit(1) + .then((rows) => rows[0] ?? null); + } + + async function listMetadataForRuns(companyId: string, runIds: string[]) { + if (runIds.length === 0) return []; + const rows = await db + .select() + .from(providerTraceRecords) + .where( + and( + eq(providerTraceRecords.companyId, companyId), + inArray(providerTraceRecords.runId, runIds.slice(0, 100)), + ), + ); + return rows.map(asMetadata); + } + + async function finalize( + runId: string, + companyId: string, + failureReason?: string | null, + ) { + const row = await getByRun(runId, companyId); + if (!row || row.deletedAt) return null; + let bytes: Buffer; + try { + bytes = await readTraceBytes(row.traceRef); + } catch { + return db + .update(providerTraceRecords) + .set({ + status: "incomplete", + reason: failureReason ?? "trace_sidecar_missing", + updatedAt: new Date(), + }) + .where(eq(providerTraceRecords.id, row.id)) + .returning() + .then((rows) => rows[0] ?? null); + } + let invalidRecordCount = 0; + const entries = bytes + .toString("utf8") + .split("\n") + .flatMap((line) => { + try { + return line.trim() ? [record(JSON.parse(line))] : []; + } catch { + invalidRecordCount += 1; + return []; + } + }); + const frames = entries.filter((entry) => entry.kind === "frame"); + const integrity = assessProviderTraceEntries(entries, { + invalidRecordCount, + requiredChannels: ["rust_native", "typescript_runnerd_rehydration"], + }); + const terminalStatus = failureReason ? "incomplete" : integrity.status; + return db + .update(providerTraceRecords) + .set({ + status: terminalStatus, + frameCount: frames.length, + byteCount: frames.reduce( + (total, frame) => total + (Number(frame.byteLength) || 0), + 0, + ), + digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`, + reason: failureReason ?? integrity.reason, + updatedAt: new Date(), + }) + .where(eq(providerTraceRecords.id, row.id)) + .returning() + .then((rows) => rows[0] ?? null); + } + + async function inspect(runId: string, companyId: string) { + const row = await getByRun(runId, companyId); + if (!row || row.deletedAt) return { trace: null, entries: [] }; + const entries = await readEntries(row.traceRef).catch(() => []); + return { + trace: asMetadata(row), + entries: entries.map((entry) => + entry.kind === "frame" ? redactedFrame(entry) : entry, + ), + }; + } + + async function revealFrame( + runId: string, + companyId: string, + frameId: number, + ) { + const row = await getByRun(runId, companyId); + if (!row || row.deletedAt) return null; + const entry = (await readEntries(row.traceRef)).find( + (candidate) => + candidate.kind === "frame" && Number(candidate.frameId) === frameId, + ); + return entry + ? (entry as unknown as ProviderTraceFrame & { kind: "frame" }) + : null; + } + + async function download(runId: string, companyId: string) { + const row = await getByRun(runId, companyId); + if (!row || row.deletedAt) return null; + return { row, bytes: await readTraceBytes(row.traceRef) }; + } + + async function remove(runId: string, companyId: string) { + const row = await getByRun(runId, companyId); + if (!row || row.deletedAt) return null; + await fs.unlink(tracePath(row.traceRef)).catch(() => undefined); + await fs.unlink(rehydrationTracePath(row.traceRef)).catch(() => undefined); + const now = new Date(); + return db + .update(providerTraceRecords) + .set({ + status: "deleted", + deletedAt: now, + updatedAt: now, + }) + .where(eq(providerTraceRecords.id, row.id)) + .returning() + .then((rows) => rows[0] ?? null); + } + + return { + prepare, + finalize, + inspect, + revealFrame, + download, + remove, + getByRun, + listMetadataForRuns, + cleanupExpired, + }; +} diff --git a/ui/src/adapters/paperclip-runner/index.test.ts b/ui/src/adapters/paperclip-runner/index.test.ts index e0c8b72b5fb..15bd3aef208 100644 --- a/ui/src/adapters/paperclip-runner/index.test.ts +++ b/ui/src/adapters/paperclip-runner/index.test.ts @@ -83,6 +83,35 @@ describe("paperclip runner transcript projection", () => { .toEqual([{ kind: "thinking", ts: expect.any(String), text: "Detailed trace", delta: true, channel: "detail" }]); }); + it("preserves empty reasoning lifecycle events as real thinking activity", () => { + const parse = paperclipRunnerUIAdapter.createStdoutParser!().parseLine; + const event = (eventType: string) => parse(JSON.stringify({ + type: "paperclip.prp.event", + event: { + eventType, + itemId: "reason-empty", + payload: { + kind: "reasoning", + channel: "summary", + item: { id: "reason-empty", type: "reasoning", text: "" }, + }, + }, + }), "2026-08-21T12:00:00.000Z"); + + expect(event("item.started")).toEqual([expect.objectContaining({ + kind: "thinking", + text: "", + lifecycle: "started", + channel: "summary", + })]); + expect(event("item.completed")).toEqual([expect.objectContaining({ + kind: "thinking", + text: "", + lifecycle: "completed", + channel: "summary", + })]); + }); + it("never exposes the structured task result envelope as final-response prose", () => { const parse = paperclipRunnerUIAdapter.createStdoutParser!().parseLine; const event = (eventType: string, payload: Record, itemId = "result-1") => parse(JSON.stringify({ @@ -99,13 +128,36 @@ describe("paperclip runner transcript projection", () => { ]); }); - it("projects canonical provider events as structured activity instead of JSON prose", () => { - const entries = paperclipRunnerUIAdapter.parseStdoutLine(JSON.stringify({ - type: "paperclip.prp.event", - event: { eventType: "plan.updated", payload: { schema: "paperclip.plan.updated.v1", planId: "plan-1", revision: 1, complete: true, explanation: "Ship safely", steps: [{ stepId: "s1", body: "Validate", status: "completed" }] } }, - }), "2026-08-21T12:00:00.000Z"); - expect(entries).toEqual([expect.objectContaining({ kind: "provider_activity", family: "plan", eventType: "plan.updated", title: "Plan" })]); - expect(entries).not.toEqual([expect.objectContaining({ kind: "assistant" })]); + it("projects every canonical provider family as structured activity instead of JSON prose", () => { + const cases = [ + ["plan.updated", "plan", { complete: true, explanation: "Ship safely" }], + ["tool.execution.completed", "tool_execution", { status: "completed", name: "tests" }], + ["research.completed", "research", { status: "completed", query: "PRP" }], + ["delegation.completed", "delegation", { status: "completed", action: "spawn" }], + ["model.route.changed", "model_identity", { provider: "claude", requestedModel: "claude", effectiveModel: "Claude Sonnet" }], + ["context.compacted", "context", { reason: "window" }], + ["artifact.generated", "artifact", { status: "completed", reference: "image.png" }], + ["review.mode.changed", "review", { state: "entered" }], + ["hook.completed", "hook", { status: "completed", event: "post-tool" }], + ["memory.citation.referenced", "memory", { label: "Decision" }], + ["safety.review.completed", "safety", { status: "completed", decision: "allowed" }], + ["terminal.input.sent", "terminal", { byteCount: 1 }], + ["wait.completed", "wait", { status: "completed", reason: "timer" }], + ["provider.notice.recorded", "provider_notice", { summary: "Provider warning" }], + ] as const; + + for (const [eventType, family, payload] of cases) { + const entries = paperclipRunnerUIAdapter.parseStdoutLine(JSON.stringify({ + type: "paperclip.prp.event", + event: { eventType, payload }, + }), "2026-08-21T12:00:00.000Z"); + expect(entries, eventType).toEqual([expect.objectContaining({ + kind: "provider_activity", + family, + eventType, + })]); + expect(entries, eventType).not.toEqual([expect.objectContaining({ kind: "assistant" })]); + } const modelRoute = paperclipRunnerUIAdapter.parseStdoutLine(JSON.stringify({ type: "paperclip.prp.event", diff --git a/ui/src/adapters/paperclip-runner/index.ts b/ui/src/adapters/paperclip-runner/index.ts index 1c4f3b8fbbf..87386d89967 100644 --- a/ui/src/adapters/paperclip-runner/index.ts +++ b/ui/src/adapters/paperclip-runner/index.ts @@ -234,15 +234,13 @@ function parseItemEvent( if (type === "reasoning") { const value = itemText(item, payload); if (phase === "completed") state.itemChannels.delete(id); - if (value && !state.reasoningDeltaItemIds.has(id)) return [{ + return [{ kind: "thinking", ts, - text: value, + text: value && !state.reasoningDeltaItemIds.has(id) ? value : "", + lifecycle: phase, channel: resolvedChannel === "detail" ? "detail" : resolvedChannel === "summary" ? "summary" : "unknown", }]; - return phase === "started" - ? [{ kind: "system", ts, text: "Reasoning started" }] - : []; } if (type === "commandexecution") { const entries = commandEntries(event, item, phase, ts); diff --git a/ui/src/api/activity.ts b/ui/src/api/activity.ts index 30698b12ffa..15651365ac0 100644 --- a/ui/src/api/activity.ts +++ b/ui/src/api/activity.ts @@ -27,6 +27,9 @@ export interface RunForIssue { continuationAttempt?: number; lastUsefulActionAt?: string | null; nextAction?: string | null; + wakeCommentIds?: string[] | null; + wakeCommentId?: string | null; + contextCommentId?: string | null; contextSnapshot?: Record | null; environment?: { id: string; diff --git a/ui/src/api/agents.ts b/ui/src/api/agents.ts index 7cc773a6eb9..9278102b856 100644 --- a/ui/src/api/agents.ts +++ b/ui/src/api/agents.ts @@ -94,6 +94,7 @@ export interface AgentWakeRequest { payload?: Record | null; idempotencyKey?: string | null; forceFreshSession?: boolean; + debug?: { providerTrace: "raw" }; } function withCompanyScope(path: string, companyId?: string) { @@ -103,14 +104,20 @@ function withCompanyScope(path: string, companyId?: string) { } function agentPath(id: string, companyId?: string, suffix = "") { - return withCompanyScope(`/agents/${encodeURIComponent(id)}${suffix}`, companyId); + return withCompanyScope( + `/agents/${encodeURIComponent(id)}${suffix}`, + companyId, + ); } export const agentsApi = { - list: (companyId: string) => api.get(`/companies/${companyId}/agents`), + list: (companyId: string) => + api.get(`/companies/${companyId}/agents`), org: (companyId: string) => api.get(`/companies/${companyId}/org`), listConfigurations: (companyId: string) => - api.get[]>(`/companies/${companyId}/agent-configurations`), + api.get[]>( + `/companies/${companyId}/agent-configurations`, + ), get: async (id: string, companyId?: string) => { try { return await api.get(agentPath(id, companyId)); @@ -131,30 +138,50 @@ export const agentsApi = { const agents = await api.get(`/companies/${companyId}/agents`); const matches = agents.filter( - (agent) => agent.status !== "terminated" && normalizeAgentUrlKey(agent.urlKey) === urlKey, + (agent) => + agent.status !== "terminated" && + normalizeAgentUrlKey(agent.urlKey) === urlKey, ); if (matches.length !== 1) throw error; return api.get(agentPath(matches[0]!.id, companyId)); } }, getConfiguration: (id: string, companyId?: string) => - api.get>(agentPath(id, companyId, "/configuration")), + api.get>( + agentPath(id, companyId, "/configuration"), + ), listConfigRevisions: (id: string, companyId?: string) => - api.get(agentPath(id, companyId, "/config-revisions")), + api.get( + agentPath(id, companyId, "/config-revisions"), + ), getConfigRevision: (id: string, revisionId: string, companyId?: string) => - api.get(agentPath(id, companyId, `/config-revisions/${revisionId}`)), - rollbackConfigRevision: (id: string, revisionId: string, companyId?: string) => - api.post(agentPath(id, companyId, `/config-revisions/${revisionId}/rollback`), {}), + api.get( + agentPath(id, companyId, `/config-revisions/${revisionId}`), + ), + rollbackConfigRevision: ( + id: string, + revisionId: string, + companyId?: string, + ) => + api.post( + agentPath(id, companyId, `/config-revisions/${revisionId}/rollback`), + {}, + ), create: (companyId: string, data: Record) => api.post(`/companies/${companyId}/agents`, data), hire: (companyId: string, data: Record) => api.post(`/companies/${companyId}/agent-hires`, data), update: (id: string, data: Record, companyId?: string) => api.patch(agentPath(id, companyId), data), - updatePermissions: (id: string, data: AgentPermissionUpdate, companyId?: string) => - api.patch(agentPath(id, companyId, "/permissions"), data), + updatePermissions: ( + id: string, + data: AgentPermissionUpdate, + companyId?: string, + ) => api.patch(agentPath(id, companyId, "/permissions"), data), instructionsBundle: (id: string, companyId?: string) => - api.get(agentPath(id, companyId, "/instructions-bundle")), + api.get( + agentPath(id, companyId, "/instructions-bundle"), + ), updateInstructionsBundle: ( id: string, data: { @@ -164,28 +191,61 @@ export const agentsApi = { clearLegacyPromptTemplate?: boolean; }, companyId?: string, - ) => api.patch(agentPath(id, companyId, "/instructions-bundle"), data), + ) => + api.patch( + agentPath(id, companyId, "/instructions-bundle"), + data, + ), instructionsFile: (id: string, relativePath: string, companyId?: string) => api.get( - agentPath(id, companyId, `/instructions-bundle/file?path=${encodeURIComponent(relativePath)}`), + agentPath( + id, + companyId, + `/instructions-bundle/file?path=${encodeURIComponent(relativePath)}`, + ), ), saveInstructionsFile: ( id: string, - data: { path: string; content: string; clearLegacyPromptTemplate?: boolean }, + data: { + path: string; + content: string; + clearLegacyPromptTemplate?: boolean; + }, companyId?: string, - ) => api.put(agentPath(id, companyId, "/instructions-bundle/file"), data), - deleteInstructionsFile: (id: string, relativePath: string, companyId?: string) => - api.delete( - agentPath(id, companyId, `/instructions-bundle/file?path=${encodeURIComponent(relativePath)}`), + ) => + api.put( + agentPath(id, companyId, "/instructions-bundle/file"), + data, ), - pause: (id: string, companyId?: string) => api.post(agentPath(id, companyId, "/pause"), {}), - resume: (id: string, companyId?: string) => api.post(agentPath(id, companyId, "/resume"), {}), + deleteInstructionsFile: ( + id: string, + relativePath: string, + companyId?: string, + ) => + api.delete( + agentPath( + id, + companyId, + `/instructions-bundle/file?path=${encodeURIComponent(relativePath)}`, + ), + ), + pause: (id: string, companyId?: string) => + api.post(agentPath(id, companyId, "/pause"), {}), + resume: (id: string, companyId?: string) => + api.post(agentPath(id, companyId, "/resume"), {}), clearError: (id: string, companyId?: string) => - api.post(agentPath(id, companyId, "/clear-error"), {}), - approve: (id: string, companyId?: string) => api.post(agentPath(id, companyId, "/approve"), {}), - terminate: (id: string, companyId?: string) => api.post(agentPath(id, companyId, "/terminate"), {}), - remove: (id: string, companyId?: string) => api.delete<{ ok: true }>(agentPath(id, companyId)), - listKeys: (id: string, companyId?: string) => api.get(agentPath(id, companyId, "/keys")), + api.post( + agentPath(id, companyId, "/clear-error"), + {}, + ), + approve: (id: string, companyId?: string) => + api.post(agentPath(id, companyId, "/approve"), {}), + terminate: (id: string, companyId?: string) => + api.post(agentPath(id, companyId, "/terminate"), {}), + remove: (id: string, companyId?: string) => + api.delete<{ ok: true }>(agentPath(id, companyId)), + listKeys: (id: string, companyId?: string) => + api.get(agentPath(id, companyId, "/keys")), skills: (id: string, companyId?: string) => api.get(agentPath(id, companyId, "/skills")), syncSkills: ( @@ -193,17 +253,33 @@ export const agentsApi = { desiredSkills: Array, mode: AgentSkillAssignmentMode, companyId?: string, - ) => api.post(agentPath(id, companyId, "/skills/sync"), { desiredSkills, mode }), - createKey: (id: string, name: string, companyId?: string, scope?: AgentApiKeyScope) => - api.post(agentPath(id, companyId, "/keys"), { name, ...(scope ? { scope } : {}) }), + ) => + api.post(agentPath(id, companyId, "/skills/sync"), { + desiredSkills, + mode, + }), + createKey: ( + id: string, + name: string, + companyId?: string, + scope?: AgentApiKeyScope, + ) => + api.post(agentPath(id, companyId, "/keys"), { + name, + ...(scope ? { scope } : {}), + }), revokeKey: (agentId: string, keyId: string, companyId?: string) => - api.delete<{ ok: true }>(agentPath(agentId, companyId, `/keys/${encodeURIComponent(keyId)}`)), + api.delete<{ ok: true }>( + agentPath(agentId, companyId, `/keys/${encodeURIComponent(keyId)}`), + ), runtimeState: (id: string, companyId?: string) => api.get(agentPath(id, companyId, "/runtime-state")), taskSessions: (id: string, companyId?: string) => api.get(agentPath(id, companyId, "/task-sessions")), resetSession: (id: string, taskKey?: string | null, companyId?: string) => - api.post(agentPath(id, companyId, "/runtime-state/reset-session"), { taskKey: taskKey ?? null }), + api.post(agentPath(id, companyId, "/runtime-state/reset-session"), { + taskKey: taskKey ?? null, + }), adapterModels: ( companyId: string, type: string, @@ -211,7 +287,8 @@ export const agentsApi = { ) => { const params = new URLSearchParams(); if (options?.refresh) params.set("refresh", "1"); - if (options?.environmentId) params.set("environmentId", options.environmentId); + if (options?.environmentId) + params.set("environmentId", options.environmentId); const query = params.size > 0 ? `?${params.toString()}` : ""; return api.get( `/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/models${query}`, @@ -239,11 +316,8 @@ export const agentsApi = { ), invoke: (id: string, companyId?: string, data: AgentWakeRequest = {}) => api.post(agentPath(id, companyId, "/heartbeat/invoke"), data), - wakeup: ( - id: string, - data: AgentWakeRequest, - companyId?: string, - ) => api.post(agentPath(id, companyId, "/wakeup"), data), + wakeup: (id: string, data: AgentWakeRequest, companyId?: string) => + api.post(agentPath(id, companyId, "/wakeup"), data), loginWithClaude: (id: string, companyId?: string) => api.post(agentPath(id, companyId, "/claude-login"), {}), startAdapterAuthLogin: ( @@ -255,11 +329,19 @@ export const agentsApi = { `/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/login-sessions`, data, ), - getAdapterAuthLoginStatus: (companyId: string, type: string, sessionId: string) => + getAdapterAuthLoginStatus: ( + companyId: string, + type: string, + sessionId: string, + ) => api.get( `/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/login-sessions/${encodeURIComponent(sessionId)}`, ), - cancelAdapterAuthLogin: (companyId: string, type: string, sessionId: string) => + cancelAdapterAuthLogin: ( + companyId: string, + type: string, + sessionId: string, + ) => api.post( `/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/login-sessions/${encodeURIComponent(sessionId)}/cancel`, {}, @@ -294,7 +376,11 @@ export const agentsApi = { api.get( `/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/prompt`, ), - submitClaudeSetupTokenBrowserCode: (companyId: string, sessionId: string, browserCode: string) => + submitClaudeSetupTokenBrowserCode: ( + companyId: string, + sessionId: string, + browserCode: string, + ) => api.post( `/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/code`, { browserCode } satisfies SubmitBrowserCodeRequest, diff --git a/ui/src/api/heartbeats.ts b/ui/src/api/heartbeats.ts index de8dac22693..c742c6fc234 100644 --- a/ui/src/api/heartbeats.ts +++ b/ui/src/api/heartbeats.ts @@ -3,6 +3,8 @@ import type { HeartbeatRunEvent, InstanceSchedulerHeartbeatAgent, WorkspaceOperation, + ProviderTraceFrame, + ProviderTraceMetadata, } from "@paperclipai/shared"; import { api } from "./client"; @@ -96,14 +98,26 @@ export interface HeartbeatRunListOptions { summary?: boolean; } +export interface ProviderTraceInspection { + trace: ProviderTraceMetadata | null; + entries: Array>; +} + export const heartbeatsApi = { - list: (companyId: string, agentId?: string, limit?: number, options: HeartbeatRunListOptions = {}) => { + list: ( + companyId: string, + agentId?: string, + limit?: number, + options: HeartbeatRunListOptions = {}, + ) => { const searchParams = new URLSearchParams(); if (agentId) searchParams.set("agentId", agentId); if (limit) searchParams.set("limit", String(limit)); if (options.summary) searchParams.set("summary", "true"); const qs = searchParams.toString(); - return api.get(`/companies/${companyId}/heartbeat-runs${qs ? `?${qs}` : ""}`); + return api.get( + `/companies/${companyId}/heartbeat-runs${qs ? `?${qs}` : ""}`, + ); }, get: (runId: string) => api.get(`/heartbeat-runs/${runId}`), events: (runId: string, afterSeq = 0, limit = 200) => @@ -111,30 +125,83 @@ export const heartbeatsApi = { `/heartbeat-runs/${runId}/events?afterSeq=${encodeURIComponent(String(afterSeq))}&limit=${encodeURIComponent(String(limit))}`, ), log: (runId: string, offset = 0, limitBytes = 256000) => - api.get<{ runId: string; store: string; logRef: string; content: string; nextOffset?: number }>( + api.get<{ + runId: string; + store: string; + logRef: string; + content: string; + nextOffset?: number; + }>( `/heartbeat-runs/${runId}/log?offset=${encodeURIComponent(String(offset))}&limitBytes=${encodeURIComponent(String(limitBytes))}`, ), workspaceOperations: (runId: string) => - api.get(`/heartbeat-runs/${runId}/workspace-operations`), - workspaceOperationLog: (operationId: string, offset = 0, limitBytes = 256000) => - api.get<{ operationId: string; store: string; logRef: string; content: string; nextOffset?: number }>( + api.get( + `/heartbeat-runs/${runId}/workspace-operations`, + ), + providerTrace: (runId: string) => + api.get(`/heartbeat-runs/${runId}/provider-trace`), + providerTraceMetadata: (companyId: string, runIds: string[]) => { + const params = new URLSearchParams({ runIds: runIds.slice(0, 100).join(",") }); + return api.get( + `/companies/${companyId}/provider-traces?${params.toString()}`, + ); + }, + revealProviderTraceFrame: (runId: string, frameId: number) => + api.post( + `/heartbeat-runs/${runId}/provider-trace/frames/${frameId}/reveal`, + {}, + ), + deleteProviderTrace: (runId: string) => + api.delete<{ ok: true }>(`/heartbeat-runs/${runId}/provider-trace`), + downloadProviderTrace: async (runId: string): Promise => { + const response = await fetch( + `/api/heartbeat-runs/${runId}/provider-trace/download`, + { + credentials: "include", + headers: { Accept: "application/x-ndjson" }, + }, + ); + if (!response.ok) { + const body = (await response.json().catch(() => null)) as { + error?: string; + } | null; + throw new Error( + body?.error ?? `Trace download failed: ${response.status}`, + ); + } + return response.blob(); + }, + workspaceOperationLog: ( + operationId: string, + offset = 0, + limitBytes = 256000, + ) => + api.get<{ + operationId: string; + store: string; + logRef: string; + content: string; + nextOffset?: number; + }>( `/workspace-operations/${operationId}/log?offset=${encodeURIComponent(String(offset))}&limitBytes=${encodeURIComponent(String(limitBytes))}`, ), - cancel: (runId: string) => api.post(`/heartbeat-runs/${runId}/cancel`, {}), + cancel: (runId: string) => + api.post(`/heartbeat-runs/${runId}/cancel`, {}), resolveRuntimeRequest: (input: { runId: string; requestId: string; turnId: string; requestKind: RuntimeRequestKind; resolution: RuntimeRequestResolution; - }) => api.post<{ accepted: true; commandId: string }>( - `/heartbeat-runs/${input.runId}/runtime-requests/${encodeURIComponent(input.requestId)}/resolve`, - { - turnId: input.turnId, - requestKind: input.requestKind, - resolution: input.resolution, - }, - ), + }) => + api.post<{ accepted: true; commandId: string }>( + `/heartbeat-runs/${input.runId}/runtime-requests/${encodeURIComponent(input.requestId)}/resolve`, + { + turnId: input.turnId, + requestKind: input.requestKind, + resolution: input.resolution, + }, + ), recordWatchdogDecision: (input: WatchdogDecisionInput) => api.post(`/heartbeat-runs/${input.runId}/watchdog-decisions`, { decision: input.decision, @@ -154,12 +221,17 @@ export const heartbeatsApi = { if (typeof options === "number") { searchParams.set("minCount", String(options)); } else if (options) { - if (options.minCount) searchParams.set("minCount", String(options.minCount)); + if (options.minCount) + searchParams.set("minCount", String(options.minCount)); if (options.limit) searchParams.set("limit", String(options.limit)); } const qs = searchParams.toString(); - return api.get(`/companies/${companyId}/live-runs${qs ? `?${qs}` : ""}`); + return api.get( + `/companies/${companyId}/live-runs${qs ? `?${qs}` : ""}`, + ); }, listInstanceSchedulerAgents: () => - api.get("/instance/scheduler-heartbeats"), + api.get( + "/instance/scheduler-heartbeats", + ), }; diff --git a/ui/src/components/AgentActionButtons.test.tsx b/ui/src/components/AgentActionButtons.test.tsx index 901717f9b7f..09be0669259 100644 --- a/ui/src/components/AgentActionButtons.test.tsx +++ b/ui/src/components/AgentActionButtons.test.tsx @@ -27,7 +27,9 @@ const mockAgentsApi = vi.hoisted(() => ({ vi.mock("@/lib/router", () => ({ Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => ( - {children} + + {children} + ), useNavigate: () => mockNavigate, })); @@ -103,11 +105,16 @@ describe("AgentActionButtons", () => { queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); - invalidateQueries = vi.spyOn(queryClient, "invalidateQueries") as unknown as ReturnType; + invalidateQueries = vi.spyOn( + queryClient, + "invalidateQueries", + ) as unknown as ReturnType; mockAgentsApi.clearError.mockResolvedValue(makeAgent({ status: "idle" })); mockAgentsApi.pause.mockResolvedValue(makeAgent({ status: "paused" })); mockAgentsApi.resume.mockResolvedValue(makeAgent({ status: "idle" })); - mockAgentsApi.terminate.mockResolvedValue(makeAgent({ status: "terminated" })); + mockAgentsApi.terminate.mockResolvedValue( + makeAgent({ status: "terminated" }), + ); mockAgentsApi.invoke.mockResolvedValue({ id: "run-1" }); mockAgentsApi.resetSession.mockResolvedValue(undefined); }); @@ -125,11 +132,19 @@ describe("AgentActionButtons", () => { vi.clearAllMocks(); }); - function render(agent: Agent, props: Partial> = {}) { + function render( + agent: Agent, + props: Partial> = {}, + ) { root ??= createRoot(container); root.render( - + , ); } @@ -143,7 +158,11 @@ describe("AgentActionButtons", () => { expect(container.textContent).toContain("Run Heartbeat"); await act(async () => { - container.querySelector('[aria-label="Open actions for Alpha Agent"]')?.click(); + container + .querySelector( + '[aria-label="Open actions for Alpha Agent"]', + ) + ?.click(); }); await flushReact(); @@ -155,18 +174,39 @@ describe("AgentActionButtons", () => { await flushReact(); await act(async () => { - container.querySelector('[aria-label="Clear error and return agent to idle"]')?.click(); + container + .querySelector( + '[aria-label="Clear error and return agent to idle"]', + ) + ?.click(); }); await flushReact(); - expect(mockAgentsApi.clearError).toHaveBeenCalledWith("agent-1", "company-1"); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "detail", "agent-1"] }); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "detail", "alpha"] }); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "runtime-state", "agent-1"] }); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "task-sessions", "agent-1"] }); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "company-1"] }); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["live-runs", "company-1"] }); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["heartbeats", "company-1", "agent-1"] }); + expect(mockAgentsApi.clearError).toHaveBeenCalledWith( + "agent-1", + "company-1", + ); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["agents", "detail", "agent-1"], + }); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["agents", "detail", "alpha"], + }); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["agents", "runtime-state", "agent-1"], + }); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["agents", "task-sessions", "agent-1"], + }); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["agents", "company-1"], + }); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["live-runs", "company-1"], + }); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["heartbeats", "company-1", "agent-1"], + }); }); it("keeps the normal pause action for non-error agents", async () => { @@ -177,18 +217,43 @@ describe("AgentActionButtons", () => { expect(container.textContent).not.toContain("Clear error"); }); + it("starts an administrator-selected run with raw provider tracing", async () => { + render(makeAgent(), { canRunWithProviderTrace: true }); + await flushReact(); + + const traceButton = Array.from(container.querySelectorAll("button")).find( + (button) => button.textContent?.includes("Run with provider trace"), + ); + expect(traceButton).toBeTruthy(); + + await act(async () => { + traceButton?.click(); + }); + await flushReact(); + + expect(mockAgentsApi.invoke).toHaveBeenCalledWith("agent-1", "company-1", { + debug: { providerTrace: "raw" }, + }); + expect(mockNavigate).toHaveBeenCalledWith("/agents/alpha/runs/run-1"); + }); + it("calls the terminate success handler after terminating an agent", async () => { const onTerminateSuccess = vi.fn(); render(makeAgent(), { onTerminateSuccess }); await flushReact(); await act(async () => { - container.querySelector('[aria-label="Open actions for Alpha Agent"]')?.click(); + container + .querySelector( + '[aria-label="Open actions for Alpha Agent"]', + ) + ?.click(); }); await flushReact(); - const terminateButton = Array.from(document.body.querySelectorAll("button")) - .find((button) => button.textContent?.includes("Terminate")); + const terminateButton = Array.from( + document.body.querySelectorAll("button"), + ).find((button) => button.textContent?.includes("Terminate")); expect(terminateButton).toBeTruthy(); await act(async () => { @@ -196,14 +261,25 @@ describe("AgentActionButtons", () => { }); await flushReact(); - expect(mockAgentsApi.terminate).toHaveBeenCalledWith("agent-1", "company-1"); - expect(onTerminateSuccess).toHaveBeenCalledWith(expect.objectContaining({ - id: "agent-1", - status: "terminated", - })); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "detail", "agent-1"] }); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "detail", "alpha"] }); - expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "company-1"] }); + expect(mockAgentsApi.terminate).toHaveBeenCalledWith( + "agent-1", + "company-1", + ); + expect(onTerminateSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + id: "agent-1", + status: "terminated", + }), + ); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["agents", "detail", "agent-1"], + }); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["agents", "detail", "alpha"], + }); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["agents", "company-1"], + }); }); it("does not terminate when navigation away from a dirty detail page is rejected", async () => { @@ -212,12 +288,17 @@ describe("AgentActionButtons", () => { await flushReact(); await act(async () => { - container.querySelector('[aria-label="Open actions for Alpha Agent"]')?.click(); + container + .querySelector( + '[aria-label="Open actions for Alpha Agent"]', + ) + ?.click(); }); await flushReact(); - const terminateButton = Array.from(document.body.querySelectorAll("button")) - .find((button) => button.textContent?.includes("Terminate")); + const terminateButton = Array.from( + document.body.querySelectorAll("button"), + ).find((button) => button.textContent?.includes("Terminate")); await act(async () => { terminateButton?.click(); }); @@ -229,10 +310,15 @@ describe("AgentActionButtons", () => { it("rechecks navigation when the form becomes dirty while termination is pending", async () => { let resolveTermination!: (agent: Agent) => void; - mockAgentsApi.terminate.mockReturnValue(new Promise((resolve) => { - resolveTermination = resolve; - })); - const onBeforeNavigate = vi.fn().mockReturnValueOnce(true).mockReturnValue(false); + mockAgentsApi.terminate.mockReturnValue( + new Promise((resolve) => { + resolveTermination = resolve; + }), + ); + const onBeforeNavigate = vi + .fn() + .mockReturnValueOnce(true) + .mockReturnValue(false); const onTerminateSuccess = vi.fn(); const agent = makeAgent(); render(agent, { @@ -243,11 +329,16 @@ describe("AgentActionButtons", () => { await flushReact(); await act(async () => { - container.querySelector('[aria-label="Open actions for Alpha Agent"]')?.click(); + container + .querySelector( + '[aria-label="Open actions for Alpha Agent"]', + ) + ?.click(); }); await flushReact(); - const terminateButton = Array.from(document.body.querySelectorAll("button")) - .find((button) => button.textContent?.includes("Terminate")); + const terminateButton = Array.from( + document.body.querySelectorAll("button"), + ).find((button) => button.textContent?.includes("Terminate")); await act(async () => { terminateButton?.click(); }); diff --git a/ui/src/components/AgentActionButtons.tsx b/ui/src/components/AgentActionButtons.tsx index a062a71af21..d974321d24e 100644 --- a/ui/src/components/AgentActionButtons.tsx +++ b/ui/src/components/AgentActionButtons.tsx @@ -11,6 +11,7 @@ import { RotateCcw, Trash2, CheckCircle2, + Bug, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -82,7 +83,12 @@ export function PauseResumeButton({ }) { if (isPaused) { return ( - @@ -139,13 +145,20 @@ export async function loadDuplicateInstructionsBundle( for (const summary of bundle.files) { const path = duplicateInstructionFilePath(bundle, summary); if (!path) continue; - const file = await agentsApi.instructionsFile(agentId, summary.path, companyId); + const file = await agentsApi.instructionsFile( + agentId, + summary.path, + companyId, + ); files[path] = file.content; } - const entryFile = Object.prototype.hasOwnProperty.call(files, bundle.entryFile) + const entryFile = Object.prototype.hasOwnProperty.call( + files, + bundle.entryFile, + ) ? bundle.entryFile - : Object.keys(files)[0] ?? "AGENTS.md"; + : (Object.keys(files)[0] ?? "AGENTS.md"); return Object.keys(files).length > 0 ? { entryFile, files } : null; } @@ -165,6 +178,7 @@ export function AgentActionButtons({ workActionsDisabled = false, workActionsDisabledReason, navigateToRunOnInvoke = true, + canRunWithProviderTrace = false, hasPendingNavigationChanges = false, onBeforeNavigate, onActionError, @@ -184,6 +198,8 @@ export function AgentActionButtons({ workActionsDisabled?: boolean; workActionsDisabledReason?: string; navigateToRunOnInvoke?: boolean; + /** Instance administrators may opt one manual run into short-lived raw provider capture. */ + canRunWithProviderTrace?: boolean; /** Whether the caller currently has an unsaved draft that navigation would discard. */ hasPendingNavigationChanges?: boolean; /** Return false to stop an action whose success would navigate away. */ @@ -220,12 +236,16 @@ export function AgentActionButtons({ pendingNavigationChangesRef.current = hasPendingNavigationChanges; beforeNavigateRef.current = onBeforeNavigate; - function confirmNavigationStart(startedDirtyRef: React.MutableRefObject) { + function confirmNavigationStart( + startedDirtyRef: React.MutableRefObject, + ) { startedDirtyRef.current = pendingNavigationChangesRef.current; return beforeNavigateRef.current?.() !== false; } - function confirmLateNavigationChanges(startedDirtyRef: React.MutableRefObject) { + function confirmLateNavigationChanges( + startedDirtyRef: React.MutableRefObject, + ) { return ( !pendingNavigationChangesRef.current || startedDirtyRef.current || @@ -250,26 +270,49 @@ export function AgentActionButtons({ ); const invalidateAgent = useCallback(() => { - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(canonicalAgentRef) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agent.id) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agent.id) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(agent.id), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(canonicalAgentRef), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.runtimeState(agent.id), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.taskSessions(agent.id), + }); if (resolvedCompanyId) { - queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); - queryClient.invalidateQueries({ queryKey: queryKeys.liveRuns(resolvedCompanyId) }); - queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(resolvedCompanyId, agent.id) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.list(resolvedCompanyId), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.liveRuns(resolvedCompanyId), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.heartbeats(resolvedCompanyId, agent.id), + }); } }, [agent.id, canonicalAgentRef, queryClient, resolvedCompanyId]); const agentAction = useMutation({ - mutationFn: async (action: "invoke" | "pause" | "resume" | "clear_error" | "approve" | "terminate") => { + mutationFn: async ( + action: + "invoke" | "pause" | "resume" | "clear_error" | "approve" | "terminate", + ) => { switch (action) { - case "invoke": return agentsApi.invoke(agent.id, resolvedCompanyId ?? undefined); - case "pause": return agentsApi.pause(agent.id, resolvedCompanyId ?? undefined); - case "resume": return agentsApi.resume(agent.id, resolvedCompanyId ?? undefined); - case "clear_error": return agentsApi.clearError(agent.id, resolvedCompanyId ?? undefined); - case "approve": return agentsApi.approve(agent.id, resolvedCompanyId ?? undefined); - case "terminate": return agentsApi.terminate(agent.id, resolvedCompanyId ?? undefined); + case "invoke": + return agentsApi.invoke(agent.id, resolvedCompanyId ?? undefined); + case "pause": + return agentsApi.pause(agent.id, resolvedCompanyId ?? undefined); + case "resume": + return agentsApi.resume(agent.id, resolvedCompanyId ?? undefined); + case "clear_error": + return agentsApi.clearError(agent.id, resolvedCompanyId ?? undefined); + case "approve": + return agentsApi.approve(agent.id, resolvedCompanyId ?? undefined); + case "terminate": + return agentsApi.terminate(agent.id, resolvedCompanyId ?? undefined); } }, onSuccess: (data, action) => { @@ -279,9 +322,17 @@ export function AgentActionButtons({ if (!confirmLateNavigationChanges(agentActionStartedDirtyRef)) return; onTerminateSuccess?.(data as Agent); } - if (action === "invoke" && navigateToRunOnInvoke && data && typeof data === "object" && "id" in data) { + if ( + action === "invoke" && + navigateToRunOnInvoke && + data && + typeof data === "object" && + "id" in data + ) { if (!confirmLateNavigationChanges(agentActionStartedDirtyRef)) return; - navigate(`/agents/${canonicalAgentRef}/runs/${(data as HeartbeatRun).id}`); + navigate( + `/agents/${canonicalAgentRef}/runs/${(data as HeartbeatRun).id}`, + ); } }, onError: (err) => { @@ -289,17 +340,44 @@ export function AgentActionButtons({ }, }); + const providerTraceAction = useMutation({ + mutationFn: () => + agentsApi.invoke(agent.id, resolvedCompanyId ?? undefined, { + debug: { providerTrace: "raw" }, + }), + onSuccess: (run) => { + onActionError?.(null); + invalidateAgent(); + if (navigateToRunOnInvoke) { + if (!confirmLateNavigationChanges(agentActionStartedDirtyRef)) return; + navigate(`/agents/${canonicalAgentRef}/runs/${run.id}`); + } + }, + onError: (err) => { + reportError( + err instanceof Error ? err.message : "Failed to start traced run", + ); + }, + }); + const duplicateAgent = useMutation({ mutationFn: async () => { if (!resolvedCompanyId) { throw new Error("Agent is not ready to duplicate"); } - const instructionsBundle = await loadDuplicateInstructionsBundle(agent.id, resolvedCompanyId); + const instructionsBundle = await loadDuplicateInstructionsBundle( + agent.id, + resolvedCompanyId, + ); const payload = buildDuplicateAgentPayload(agent, instructionsBundle); try { return await agentsApi.create(resolvedCompanyId, payload); } catch (error) { - if (error instanceof ApiError && error.status === 409 && error.message.includes("requires board approval")) { + if ( + error instanceof ApiError && + error.status === 409 && + error.message.includes("requires board approval") + ) { const hire = await agentsApi.hire(resolvedCompanyId, payload); return hire.agent; } @@ -309,16 +387,27 @@ export function AgentActionButtons({ onSuccess: async (createdAgent) => { onActionError?.(null); if (resolvedCompanyId) { - await queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); + await queryClient.invalidateQueries({ + queryKey: queryKeys.agents.list(resolvedCompanyId), + }); } - pushToast({ title: "Agent duplicated", body: createdAgent.name, tone: "success" }); + pushToast({ + title: "Agent duplicated", + body: createdAgent.name, + tone: "success", + }); if (!confirmLateNavigationChanges(duplicateStartedDirtyRef)) return; navigate(`/agents/${agentRouteRef(createdAgent)}/dashboard`); }, onError: (err) => { - const message = err instanceof Error ? err.message : "Failed to duplicate agent"; + const message = + err instanceof Error ? err.message : "Failed to duplicate agent"; onActionError?.(message); - pushToast({ title: "Could not duplicate agent", body: message, tone: "error" }); + pushToast({ + title: "Could not duplicate agent", + body: message, + tone: "error", + }); }, }); @@ -332,25 +421,50 @@ export function AgentActionButtons({ }, [agent.name, duplicateAgent]); const resetTaskSession = useMutation({ - mutationFn: () => agentsApi.resetSession(agent.id, null, resolvedCompanyId ?? undefined), + mutationFn: () => + agentsApi.resetSession(agent.id, null, resolvedCompanyId ?? undefined), onSuccess: () => { onActionError?.(null); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agent.id) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agent.id) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.runtimeState(agent.id), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.taskSessions(agent.id), + }); }, onError: (err) => { - reportError(err instanceof Error ? err.message : "Failed to reset session"); + reportError( + err instanceof Error ? err.message : "Failed to reset session", + ); }, }); const isPendingApproval = agent.status === "pending_approval"; - const disabled = actionsDisabled || agentAction.isPending; - const assignAndRunDisabled = disabled || isPendingApproval || workActionsDisabled; - const pauseResumeDisabled = disabled || isPendingApproval || (isPaused && workActionsDisabled); + const disabled = + actionsDisabled || agentAction.isPending || providerTraceAction.isPending; + const assignAndRunDisabled = + disabled || isPendingApproval || workActionsDisabled; + const pauseResumeDisabled = + disabled || isPendingApproval || (isPaused && workActionsDisabled); const clearErrorDisabled = disabled; + const runtimeConfig = agent.runtimeConfig as Record | null; + const runtimeDebug = + runtimeConfig && typeof runtimeConfig.debug === "object" && runtimeConfig.debug !== null + ? (runtimeConfig.debug as Record) + : null; + const persistentProviderTrace = runtimeDebug?.providerTrace === "raw"; return (
+ {persistentProviderTrace ? ( + + + Raw tracing on + + ) : null} + )} {isError ? ( agentAction.mutate("clear_error")} @@ -379,7 +516,11 @@ export function AgentActionButtons({ ) : ( (pauseConfirm ? setPauseConfirmOpen(true) : agentAction.mutate("pause"))} + onPause={() => + pauseConfirm + ? setPauseConfirmOpen(true) + : agentAction.mutate("pause") + } onResume={() => agentAction.mutate("resume")} disabled={pauseResumeDisabled} size={size} @@ -411,7 +552,11 @@ export function AgentActionButtons({ {children} - @@ -432,7 +577,11 @@ export function AgentActionButtons({ className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50" onClick={() => { void copyTextToClipboard(agent.id).catch(() => { - pushToast({ title: "Copy failed", body: "Clipboard access is unavailable.", tone: "error" }); + pushToast({ + title: "Copy failed", + body: "Clipboard access is unavailable.", + tone: "error", + }); }); setMoreOpen(false); }} @@ -455,7 +604,11 @@ export function AgentActionButtons({ className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-destructive" onClick={() => { setMoreOpen(false); - if (onTerminateSuccess && !confirmNavigationStart(agentActionStartedDirtyRef)) return; + if ( + onTerminateSuccess && + !confirmNavigationStart(agentActionStartedDirtyRef) + ) + return; agentAction.mutate("terminate"); }} > diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index aded4092e30..8e5b8479b8e 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -28,7 +28,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; -import { FolderOpen, Heart, ChevronDown, X, Copy, Check, ExternalLink, Loader2, TriangleAlert } from "lucide-react"; +import { FolderOpen, Heart, ChevronDown, X, Copy, Check, ExternalLink, Loader2, TriangleAlert, Bug } from "lucide-react"; import { asBoolean, asFiniteNumber, asObject, cn } from "../lib/utils"; import { copyTextToClipboard } from "../lib/clipboard"; import { resolveAdapterTestEnvironmentId } from "../lib/adapter-test-environment"; @@ -127,6 +127,7 @@ const emptyOverlay: AgentConfigOverlay = { identity: {}, adapterConfig: {}, heartbeat: {}, + debug: {}, runtime: {}, }; @@ -143,6 +144,7 @@ function isOverlayDirty(o: AgentConfigOverlay): boolean { o.adapterType !== undefined || Object.keys(o.adapterConfig).length > 0 || Object.keys(o.heartbeat).length > 0 || + Object.keys(o.debug).length > 0 || Object.keys(o.runtime).length > 0 || o.modelProfiles?.cheap !== undefined ); @@ -340,7 +342,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { const isDirty = !isCreate && isOverlayDirty(overlay); - type RecordOverlayGroup = "identity" | "adapterConfig" | "heartbeat" | "runtime"; + type RecordOverlayGroup = "identity" | "adapterConfig" | "heartbeat" | "debug" | "runtime"; /** Read effective value: overlay if dirty, else original */ function eff(group: RecordOverlayGroup, field: string, original: T): T { @@ -426,6 +428,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { const config = !isCreate ? ((props.agent.adapterConfig ?? {}) as Record) : {}; const runtimeConfig = !isCreate ? ((props.agent.runtimeConfig ?? {}) as Record) : {}; const heartbeat = !isCreate ? ((runtimeConfig.heartbeat ?? {}) as Record) : {}; + const debug = !isCreate ? ((runtimeConfig.debug ?? {}) as Record) : {}; const adapterType = isCreate ? props.values.adapterType @@ -1904,6 +1907,46 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
) : null} + {/* ---- Debugging ---- */} + {!isCreate ? ( +
+ {cards ? ( +

+ Debugging +

+ ) : ( +
+ Debugging +
+ )} +
+ ("debug", "providerTrace", debug.providerTrace) === "raw"} + onChange={(enabled) => + mark("debug", "providerTrace", enabled ? "raw" : undefined) + } + /> + {eff("debug", "providerTrace", debug.providerTrace) === "raw" ? ( +
+ + + Raw tracing is on for future runs. Paperclip keeps at most 64 MiB per run and automatically deletes it after 24 hours. + +
+ ) : null} +
+
+ ) : null} + ); } diff --git a/ui/src/components/IssueRunLedger.test.tsx b/ui/src/components/IssueRunLedger.test.tsx index 586e462a978..efdb52125f0 100644 --- a/ui/src/components/IssueRunLedger.test.tsx +++ b/ui/src/components/IssueRunLedger.test.tsx @@ -528,7 +528,11 @@ describe("IssueRunLedger", () => { expect(container.textContent).not.toContain("Continue monitoring"); expect(container.textContent).not.toContain("Snooze 1h"); expect(container.textContent).not.toContain("Mark false positive"); - expect(container.querySelectorAll("button")).toHaveLength(0); + expect( + Array.from(container.querySelectorAll("button")).filter((button) => + /continue|snooze|false positive/i.test(button.textContent ?? ""), + ), + ).toHaveLength(0); expect(onWatchdogDecision).not.toHaveBeenCalled(); }); diff --git a/ui/src/components/IssueRunLedger.tsx b/ui/src/components/IssueRunLedger.tsx index a69693b4b72..af23d834339 100644 --- a/ui/src/components/IssueRunLedger.tsx +++ b/ui/src/components/IssueRunLedger.tsx @@ -1,10 +1,17 @@ -import { useMemo, useState, type ReactNode } from "react"; -import type { ActivityEvent, Issue, Agent } from "@paperclipai/shared"; -import { isResponsibleUserDenialCode, responsibleUserLabel } from "@paperclipai/shared"; +import { useEffect, useMemo, useState, type ReactNode } from "react"; +import type { ActivityEvent, Issue, Agent, ProviderTraceMetadata } from "@paperclipai/shared"; +import { + isResponsibleUserDenialCode, + responsibleUserLabel, +} from "@paperclipai/shared"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link } from "@/lib/router"; import { accessApi, type CurrentBoardAccess } from "../api/access"; -import { activityApi, type RunForIssue, type RunLivenessState } from "../api/activity"; +import { + activityApi, + type RunForIssue, + type RunLivenessState, +} from "../api/activity"; import { ApiError } from "../api/client"; import { heartbeatsApi, @@ -20,6 +27,12 @@ import { describeRunRetryState } from "../lib/runRetryState"; import { readSourceResolvedWatchdogFold } from "../lib/source-resolved-watchdog-fold"; import { SourceResolvedFoldBadge } from "./SourceResolvedFoldBadge"; import { ResponsibleUserDenialNotice } from "./ResponsibleUserDenialNotice"; +import { RunnerInspector } from "./RunnerInspector"; +import { agentsApi } from "../api/agents"; +import { + ProviderTraceStatusBadge, + runRequestedProviderTrace, +} from "./ProviderTraceStatusBadge"; type IssueRunLedgerProps = { issueId: string; @@ -47,6 +60,8 @@ type IssueRunLedgerContentProps = { canRecordWatchdogDecisions?: boolean; watchdogDecisionError?: string | null; onWatchdogDecision?: (input: WatchdogDecisionInput) => void; + onRerunWithTrace?: (run: RunForIssue) => void; + providerTraceMetadata?: ReadonlyMap; }; type LedgerRun = RunForIssue & { @@ -109,7 +124,8 @@ const LIVENESS_COPY: Record = { needs_followup: { label: "Needs follow-up", tone: "border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-300", - description: "Run produced useful output but did not prove concrete progress.", + description: + "Run produced useful output but did not prove concrete progress.", }, }; @@ -134,14 +150,18 @@ const MISSING_LIVENESS_COPY: LivenessCopy = { const TERMINAL_CHILD_STATUSES = new Set(["done", "cancelled"]); const ACTIVE_RUN_STATUSES = new Set(["queued", "running"]); -type RunOutputSilenceLevel = NonNullable["level"]; +type RunOutputSilenceLevel = NonNullable< + ActiveRunForIssue["outputSilence"] +>["level"]; type RunOutputSilenceCopy = { label: string; tone: string; }; -const RUN_OUTPUT_SILENCE_COPY: Partial> = { +const RUN_OUTPUT_SILENCE_COPY: Partial< + Record +> = { suspicious: { label: "Silence watch", tone: "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300", @@ -157,12 +177,15 @@ const RUN_OUTPUT_SILENCE_COPY: Partial | null { - if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + if (typeof value !== "object" || value === null || Array.isArray(value)) + return null; return value as Record; } function readString(value: unknown) { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : null; } interface ModelProfileSummary { @@ -208,7 +231,10 @@ function readNumber(value: unknown) { return typeof value === "number" && Number.isFinite(value) ? value : null; } -function formatDuration(start: string | Date | null | undefined, end: string | Date | null | undefined) { +function formatDuration( + start: string | Date | null | undefined, + end: string | Date | null | undefined, +) { if (!start) return null; const startMs = new Date(start).getTime(); const endMs = end ? new Date(end).getTime() : Date.now(); @@ -217,7 +243,8 @@ function formatDuration(start: string | Date | null | undefined, end: string | D if (totalSeconds < 60) return `${totalSeconds}s`; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; - if (minutes < 60) return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; + if (minutes < 60) + return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`; @@ -228,7 +255,9 @@ function toIsoString(value: string | Date | null | undefined) { return value instanceof Date ? value.toISOString() : value; } -function liveRunToLedgerRun(run: LiveRunForIssue | ActiveRunForIssue): LedgerRun { +function liveRunToLedgerRun( + run: LiveRunForIssue | ActiveRunForIssue, +): LedgerRun { return { runId: run.id, status: run.status, @@ -258,7 +287,12 @@ function mergeRuns( byId.set( run.id, existing - ? { ...existing, isLive: true, agentName: run.agentName, outputSilence: run.outputSilence } + ? { + ...existing, + isLive: true, + agentName: run.agentName, + outputSilence: run.outputSilence, + } : liveRunToLedgerRun(run), ); } @@ -292,11 +326,15 @@ function isActiveRun(run: Pick) { return run.isLive || ACTIVE_RUN_STATUSES.has(run.status); } -function runSummary(run: LedgerRun, agentMap: ReadonlyMap>) { +function runSummary( + run: LedgerRun, + agentMap: ReadonlyMap>, +) { const agentName = compactAgentName(run, agentMap); if (run.status === "running") return `Running now by ${agentName}`; if (run.status === "queued") return `Queued for ${agentName}`; - if (run.status === "scheduled_retry") return `Automatic retry scheduled for ${agentName}`; + if (run.status === "scheduled_retry") + return `Automatic retry scheduled for ${agentName}`; return `${statusLabel(run.status)} by ${agentName}`; } @@ -312,19 +350,27 @@ function stopReasonLabel(run: RunForIssue) { const timeoutFired = result?.timeoutFired === true; const effectiveTimeoutSec = readNumber(result?.effectiveTimeoutSec); const timeoutText = - effectiveTimeoutSec && effectiveTimeoutSec > 0 ? `${effectiveTimeoutSec}s timeout` : null; + effectiveTimeoutSec && effectiveTimeoutSec > 0 + ? `${effectiveTimeoutSec}s timeout` + : null; if (timeoutFired || stopReason === "timeout") { return timeoutText ? `timeout (${timeoutText})` : "timeout"; } - if (stopReason === "max_turns_exhausted" || stopReason === "turn_limit_exhausted") return "max turns exhausted"; + if ( + stopReason === "max_turns_exhausted" || + stopReason === "turn_limit_exhausted" + ) + return "max turns exhausted"; if (stopReason === "budget_paused") return "budget paused"; if (stopReason === "cancelled") return "cancelled"; if (stopReason === "paused") return "paused by board"; if (stopReason === "process_lost") return "process lost"; - if (stopReason === "unmanaged_background_task_stopped") return "unmanaged background task stopped"; + if (stopReason === "unmanaged_background_task_stopped") + return "unmanaged background task stopped"; if (stopReason === "adapter_failed") return "adapter failed"; - if (stopReason === "completed") return timeoutText ? `completed (${timeoutText})` : "completed"; + if (stopReason === "completed") + return timeoutText ? `completed (${timeoutText})` : "completed"; return timeoutText; } @@ -341,7 +387,10 @@ function lastUsefulActionLabel(run: LedgerRun) { if (run.status === "scheduled_retry") return "Waiting for next attempt"; if (run.lastUsefulActionAt) return relativeTime(run.lastUsefulActionAt); if (isActiveRun(run)) return "No action recorded yet"; - if (run.livenessState === "plan_only" || run.livenessState === "needs_followup") { + if ( + run.livenessState === "plan_only" || + run.livenessState === "needs_followup" + ) { return "No concrete action"; } if (run.livenessState === "empty_response") return "No useful output"; @@ -359,21 +408,31 @@ function hasExhaustedContinuation(run: RunForIssue) { } function childIssueSummary(childIssues: Issue[]) { - const active = childIssues.filter((issue) => !TERMINAL_CHILD_STATUSES.has(issue.status)); + const active = childIssues.filter( + (issue) => !TERMINAL_CHILD_STATUSES.has(issue.status), + ); const done = childIssues.filter((issue) => issue.status === "done").length; - const cancelled = childIssues.filter((issue) => issue.status === "cancelled").length; + const cancelled = childIssues.filter( + (issue) => issue.status === "cancelled", + ).length; return { active, done, cancelled, total: childIssues.length }; } -function compactAgentName(run: LedgerRun, agentMap: ReadonlyMap>) { - return run.agentName ?? agentMap.get(run.agentId)?.name ?? run.agentId.slice(0, 8); +function compactAgentName( + run: LedgerRun, + agentMap: ReadonlyMap>, +) { + return ( + run.agentName ?? agentMap.get(run.agentId)?.name ?? run.agentId.slice(0, 8) + ); } function formatSilenceAge(ms: number | null | undefined) { if (!ms || ms <= 0) return null; const totalMinutes = Math.floor(ms / 60_000); if (totalMinutes < 1) return "under 1 minute"; - if (totalMinutes < 60) return `${totalMinutes} minute${totalMinutes === 1 ? "" : "s"}`; + if (totalMinutes < 60) + return `${totalMinutes} minute${totalMinutes === 1 ? "" : "s"}`; const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; if (minutes === 0) return `${hours} hour${hours === 1 ? "" : "s"}`; @@ -385,13 +444,19 @@ function canBoardRecordWatchdogDecision( boardAccess: CurrentBoardAccess | undefined, ) { if (!boardAccess) return false; - if (boardAccess.source === "local_implicit" || boardAccess.isInstanceAdmin) return true; + if (boardAccess.source === "local_implicit" || boardAccess.isInstanceAdmin) + return true; const membership = boardAccess.memberships?.find( (item) => item.companyId === companyId && item.status === "active", ); - if (!membership) return boardAccess.companyIds.includes(companyId) && !boardAccess.memberships; - return membership.membershipRole !== "viewer" && membership.membershipRole !== null; + if (!membership) + return ( + boardAccess.companyIds.includes(companyId) && !boardAccess.memberships + ); + return ( + membership.membershipRole !== "viewer" && membership.membershipRole !== null + ); } function watchdogDecisionErrorMessage(error: unknown) { @@ -416,7 +481,9 @@ export function IssueRunLedger({ }: IssueRunLedgerProps) { const queryClient = useQueryClient(); const { pushToast } = useToastActions(); - const [watchdogDecisionError, setWatchdogDecisionError] = useState(null); + const [watchdogDecisionError, setWatchdogDecisionError] = useState< + string | null + >(null); const { data: boardAccess } = useQuery({ queryKey: queryKeys.access.currentBoardAccess, queryFn: () => accessApi.getCurrentBoardAccess(), @@ -425,7 +492,8 @@ export function IssueRunLedger({ const { data: runs } = useQuery({ queryKey: queryKeys.issues.runs(issueId), queryFn: () => activityApi.runsForIssue(issueId), - refetchInterval: hasLiveRuns || issueStatus === "in_progress" ? 5000 : false, + refetchInterval: + hasLiveRuns || issueStatus === "in_progress" ? 5000 : false, placeholderData: keepPreviousDataForSameQueryTail(issueId), }); const { data: liveRuns } = useQuery({ @@ -433,28 +501,53 @@ export function IssueRunLedger({ queryFn: () => heartbeatsApi.liveRunsForIssue(issueId), enabled: hasLiveRuns, refetchInterval: 3000, - placeholderData: keepPreviousDataForSameQueryTail(issueId), + placeholderData: + keepPreviousDataForSameQueryTail(issueId), }); const { data: activeRun = null } = useQuery({ queryKey: queryKeys.issues.activeRun(issueId), queryFn: () => heartbeatsApi.activeRunForIssue(issueId), enabled: hasLiveRuns || issueStatus === "in_progress", refetchInterval: hasLiveRuns ? false : 3000, - placeholderData: keepPreviousDataForSameQueryTail(issueId), + placeholderData: keepPreviousDataForSameQueryTail( + issueId, + ), }); + const traceRunIds = useMemo( + () => (runs ?? []).slice(0, 100).map((run) => run.runId), + [runs], + ); + const canInspectProviderTrace = + boardAccess?.source === "local_implicit" || boardAccess?.isInstanceAdmin === true; + const { data: providerTraceRows } = useQuery({ + queryKey: queryKeys.providerTraceMetadata(companyId, traceRunIds), + queryFn: () => heartbeatsApi.providerTraceMetadata(companyId, traceRunIds), + enabled: canInspectProviderTrace && traceRunIds.length > 0, + retry: false, + }); + const providerTraceMetadata = useMemo( + () => new Map((providerTraceRows ?? []).map((trace) => [trace.runId, trace])), + [providerTraceRows], + ); const watchdogDecision = useMutation({ - mutationFn: (input: WatchdogDecisionInput) => heartbeatsApi.recordWatchdogDecision(input), + mutationFn: (input: WatchdogDecisionInput) => + heartbeatsApi.recordWatchdogDecision(input), onMutate: () => { setWatchdogDecisionError(null); }, onSuccess: () => { setWatchdogDecisionError(null); - queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(issueId) }); - queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(issueId) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.issues.activeRun(issueId), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.issues.liveRuns(issueId), + }); }, onError: (error) => { const message = watchdogDecisionErrorMessage(error); - const dedupeSuffix = error instanceof ApiError ? String(error.status) : "error"; + const dedupeSuffix = + error instanceof ApiError ? String(error.status) : "error"; setWatchdogDecisionError(message); pushToast({ title: "Watchdog decision not recorded", @@ -464,6 +557,48 @@ export function IssueRunLedger({ }); }, }); + const rerunWithTrace = useMutation({ + mutationFn: async (run: RunForIssue) => { + const context = asRecord(run.contextSnapshot); + const payload: Record = {}; + for (const key of ["issueId", "taskId", "taskKey"] as const) { + const value = readString(context?.[key]); + if (value) payload[key] = value; + } + const result = await agentsApi.wakeup( + run.agentId, + { + source: "on_demand", + triggerDetail: "manual", + reason: "rerun_with_provider_trace", + payload, + debug: { providerTrace: "raw" }, + }, + companyId, + ); + if (!("id" in result)) + throw new Error(result.message ?? "Trace re-run was skipped."); + return result; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.issues.runs(issueId), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.issues.liveRuns(issueId), + }); + }, + onError: (error) => + pushToast({ + title: "Trace re-run not started", + body: + error instanceof Error + ? error.message + : "Paperclip could not start the trace re-run.", + tone: "error", + dedupeKey: `provider-trace-rerun:${issueId}`, + }), + }); return ( watchdogDecision.mutate(input)} + onRerunWithTrace={(run) => rerunWithTrace.mutate(run)} + providerTraceMetadata={providerTraceMetadata} /> ); } @@ -498,14 +638,29 @@ export function IssueRunLedgerContent({ canRecordWatchdogDecisions = true, watchdogDecisionError, onWatchdogDecision, + onRerunWithTrace, + providerTraceMetadata = new Map(), }: IssueRunLedgerContentProps) { - const ledgerRuns = useMemo(() => mergeRuns(runs, liveRuns, activeRun), [activeRun, liveRuns, runs]); + const [inspectedRun, setInspectedRun] = useState(null); + const ledgerRuns = useMemo( + () => mergeRuns(runs, liveRuns, activeRun), + [activeRun, liveRuns, runs], + ); + useEffect(() => { + if (inspectedRun || typeof window === "undefined") return; + const requestedRunId = new URLSearchParams(window.location.search).get("inspectRun"); + if (!requestedRunId) return; + const requestedRun = ledgerRuns.find((run) => run.runId === requestedRunId); + if (requestedRun) setInspectedRun(requestedRun); + }, [inspectedRun, ledgerRuns]); const latestRun = ledgerRuns[0] ?? null; const latestSilentRun = useMemo( () => - ledgerRuns.find((run) => - isActiveRun(run) - && (run.outputSilence?.level === "critical" || run.outputSilence?.level === "suspicious"), + ledgerRuns.find( + (run) => + isActiveRun(run) && + (run.outputSilence?.level === "critical" || + run.outputSilence?.level === "suspicious"), ) ?? null, [ledgerRuns], ); @@ -526,9 +681,10 @@ export function IssueRunLedgerContent({ items.push({ kind: "activity", id: event.id, - timestamp: event.createdAt instanceof Date - ? event.createdAt.toISOString() - : String(event.createdAt), + timestamp: + event.createdAt instanceof Date + ? event.createdAt.toISOString() + : String(event.createdAt), event, }); } @@ -546,7 +702,9 @@ export function IssueRunLedgerContent({
-

Run ledger

+

+ Run ledger +

{latestRun ? runSummary(latestRun, agentMap) @@ -583,9 +741,13 @@ export function IssueRunLedgerContent({ to={`/issues/${child.identifier ?? child.id}`} className="inline-flex min-w-0 max-w-full items-center gap-1 rounded-md border border-border bg-background px-2 py-1 text-(length:--text-micro) hover:bg-accent/40" > - {child.identifier ?? child.id.slice(0, 8)} + + {child.identifier ?? child.id.slice(0, 8)} + {child.title} - {statusLabel(child.status)} + + {statusLabel(child.status)} + ))} {children.active.length > 4 ? ( @@ -614,7 +776,9 @@ export function IssueRunLedgerContent({

Latest active run has been silent for{" "} - {formatSilenceAge(latestSilentRun.outputSilence.silenceAgeMs) ?? "an extended period"}. + {formatSilenceAge(latestSilentRun.outputSilence.silenceAgeMs) ?? + "an extended period"} + . {latestSilentRun.outputSilence.evaluationIssueIdentifier ? ( <> {" "} @@ -624,8 +788,8 @@ export function IssueRunLedgerContent({ className="font-medium underline underline-offset-2" > {latestSilentRun.outputSilence.evaluationIssueIdentifier} - - {" "}for recovery context. + {" "} + for recovery context. ) : null}

@@ -638,8 +802,10 @@ export function IssueRunLedgerContent({ onWatchdogDecision({ runId: latestSilentRun.runId, decision: "continue", - evaluationIssueId: latestSilentRun.outputSilence?.evaluationIssueId ?? null, - })} + evaluationIssueId: + latestSilentRun.outputSilence?.evaluationIssueId ?? null, + }) + } disabled={pendingWatchdogDecision != null} > Continue monitoring @@ -651,10 +817,14 @@ export function IssueRunLedgerContent({ onWatchdogDecision({ runId: latestSilentRun.runId, decision: "snooze", - evaluationIssueId: latestSilentRun.outputSilence?.evaluationIssueId ?? null, - snoozedUntil: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + evaluationIssueId: + latestSilentRun.outputSilence?.evaluationIssueId ?? null, + snoozedUntil: new Date( + Date.now() + 60 * 60 * 1000, + ).toISOString(), reason: "Snoozed from issue run ledger", - })} + }) + } disabled={pendingWatchdogDecision != null} > Snooze 1h @@ -666,9 +836,11 @@ export function IssueRunLedgerContent({ onWatchdogDecision({ runId: latestSilentRun.runId, decision: "dismissed_false_positive", - evaluationIssueId: latestSilentRun.outputSilence?.evaluationIssueId ?? null, + evaluationIssueId: + latestSilentRun.outputSilence?.evaluationIssueId ?? null, reason: "Dismissed from issue run ledger", - })} + }) + } disabled={pendingWatchdogDecision != null} > Mark false positive @@ -693,7 +865,11 @@ export function IssueRunLedgerContent({
{feedItems.slice(0, 20).map((item) => { if (item.kind === "activity") { - return
{renderActivityEvent?.(item.event)}
; + return ( +
+ {renderActivityEvent?.(item.event)} +
+ ); } const run = item.run; const liveness = livenessCopyForRun(run); @@ -706,8 +882,12 @@ export function IssueRunLedgerContent({ const onBehalfOfLabel = run.responsibleUserId ? responsibleUserLabel(resolveUserLabel?.(run.responsibleUserId)) : null; - const denialCode = isResponsibleUserDenialCode(run.errorCode) ? run.errorCode : null; - const sourceResolvedFold = readSourceResolvedWatchdogFold(run.resultJson); + const denialCode = isResponsibleUserDenialCode(run.errorCode) + ? run.errorCode + : null; + const sourceResolvedFold = readSourceResolvedWatchdogFold( + run.resultJson, + ); return (
- on behalf of {onBehalfOfLabel} + on behalf of{" "} + {onBehalfOfLabel} ) : null} @@ -740,6 +921,11 @@ export function IssueRunLedgerContent({ live ) : null} + ) : null} {continuation ? ( - {continuation} + + {continuation} + ) : null} {retryState ? ( ) : null} - {run.outputSilence && RUN_OUTPUT_SILENCE_COPY[run.outputSilence.level] ? ( + {run.outputSilence && + RUN_OUTPUT_SILENCE_COPY[run.outputSilence.level] ? ( { const profile = modelProfileForRun(run); if (!profile) return null; - const label = profile.applied === profile.requested - ? `Profile: ${profile.requested}` - : profile.applied - ? `Profile: ${profile.requested} → ${profile.applied}` - : `Profile: ${profile.requested} (unavailable)`; + const label = + profile.applied === profile.requested + ? `Profile: ${profile.requested}` + : profile.applied + ? `Profile: ${profile.requested} → ${profile.applied}` + : `Profile: ${profile.requested} (unavailable)`; return ( : null} - {relativeTime(item.timestamp)} + + {relativeTime(item.timestamp)} + +
@@ -819,7 +1018,9 @@ export function IssueRunLedgerContent({ {retryState ? (
{retryState.detail ?

{retryState.detail}

: null} - {retryState.secondary ?

{retryState.secondary}

: null} + {retryState.secondary ? ( +

{retryState.secondary}

+ ) : null} {retryState.retryOfRunId ? (

Retry of{" "} @@ -836,14 +1037,20 @@ export function IssueRunLedgerContent({ {(() => { const profile = modelProfileForRun(run); - if (!profile?.fallbackReason || profile.applied === profile.requested) return null; + if ( + !profile?.fallbackReason || + profile.applied === profile.requested + ) + return null; return (

{profile.requested === "cheap" ? "Cheap profile fell back to primary" : `${profile.requested} profile unavailable`} {": "} - {profile.fallbackReason} + + {profile.fallbackReason} +

); })()} @@ -857,14 +1064,22 @@ export function IssueRunLedgerContent({ {denialCode ? ( ) : null} {run.nextAction ? (
- Next action: - {run.nextAction} + + Next action:{" "} + + + {run.nextAction} +
) : null} @@ -877,6 +1092,22 @@ export function IssueRunLedgerContent({ ) : null}
)} + {inspectedRun ? ( + { + if (!nextOpen) setInspectedRun(null); + }} + onRerunWithTrace={ + !["queued", "running"].includes(inspectedRun.status) && + onRerunWithTrace + ? () => onRerunWithTrace(inspectedRun) + : undefined + } + /> + ) : null}
); } diff --git a/ui/src/components/ProviderTraceStatusBadge.tsx b/ui/src/components/ProviderTraceStatusBadge.tsx new file mode 100644 index 00000000000..5f70abcabae --- /dev/null +++ b/ui/src/components/ProviderTraceStatusBadge.tsx @@ -0,0 +1,83 @@ +import type { ProviderTraceMetadata } from "@paperclipai/shared"; +import { Bug, CircleOff } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export function runRequestedProviderTrace( + contextSnapshot: Record | null | undefined, +) { + if (!contextSnapshot) return false; + const debug = contextSnapshot.debug; + return ( + typeof debug === "object" && + debug !== null && + !Array.isArray(debug) && + (debug as Record).providerTrace === "raw" + ); +} + +export function ProviderTraceStatusBadge({ + trace, + requested = false, + showOff = false, + className, +}: { + trace?: ProviderTraceMetadata | null; + requested?: boolean; + showOff?: boolean; + className?: string; +}) { + const status = trace?.status; + const expired = trace + ? new Date(trace.expiresAt).getTime() <= Date.now() + : false; + const label = expired + ? "Trace expired" + : status === "capturing" + ? "Raw tracing enabled" + : status === "complete" + ? "Trace captured" + : status === "incomplete" + ? "Trace incomplete" + : status === "truncated" + ? "Trace truncated" + : status === "expired" + ? "Trace expired" + : status === "deleted" + ? "Trace deleted" + : requested + ? "Trace requested" + : showOff + ? "Trace off" + : null; + if (!label) return null; + const warning = + status === "incomplete" || + status === "truncated" || + status === "expired" || + status === "deleted" || + expired; + const Icon = label === "Trace off" ? CircleOff : Bug; + return ( + + + {label} + + ); +} diff --git a/ui/src/components/RunnerInspector.test.tsx b/ui/src/components/RunnerInspector.test.tsx new file mode 100644 index 00000000000..233d6672b57 --- /dev/null +++ b/ui/src/components/RunnerInspector.test.tsx @@ -0,0 +1,339 @@ +// @vitest-environment jsdom + +import { createElement, type ReactNode } from "react"; +import { flushSync } from "react-dom"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { RunnerInspector } from "./RunnerInspector"; + +const accessMock = vi.hoisted(() => vi.fn()); +const eventsMock = vi.hoisted(() => vi.fn()); +const traceMock = vi.hoisted(() => vi.fn()); +const revealMock = vi.hoisted(() => vi.fn()); + +vi.mock("@/api/access", () => ({ + accessApi: { getCurrentBoardAccess: accessMock }, +})); + +vi.mock("@/api/heartbeats", () => ({ + heartbeatsApi: { + events: eventsMock, + providerTrace: traceMock, + revealProviderTraceFrame: revealMock, + downloadProviderTrace: vi.fn(), + deleteProviderTrace: vi.fn(), + }, +})); + +vi.mock("@/components/ui/sheet", () => ({ + Sheet: ({ open, children }: { open: boolean; children: ReactNode }) => + open ? createElement("div", null, children) : null, + SheetContent: ({ children }: { children: ReactNode }) => + createElement("div", null, children), + SheetDescription: ({ children }: { children: ReactNode }) => + createElement("p", null, children), + SheetHeader: ({ children }: { children: ReactNode }) => + createElement("header", null, children), + SheetTitle: ({ children }: { children: ReactNode }) => + createElement("h2", null, children), +})); + +vi.mock("@/components/ui/select", () => ({ + Select: ({ children }: { children: ReactNode }) => + createElement("div", null, children), + SelectContent: ({ children }: { children: ReactNode }) => + createElement("div", null, children), + SelectItem: ({ children }: { children: ReactNode }) => + createElement("span", null, children), + SelectTrigger: ({ children }: { children: ReactNode }) => + createElement("button", { type: "button" }, children), + SelectValue: ({ placeholder }: { placeholder?: string }) => + createElement("span", null, placeholder), +})); + +vi.mock("@/components/ui/scroll-area", () => ({ + ScrollArea: ({ children }: { children: ReactNode }) => + createElement("div", null, children), +})); + +( + globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean } +).IS_REACT_ACT_ENVIRONMENT = true; + +async function flush() { + for (let index = 0; index < 5; index += 1) { + await Promise.resolve(); + await new Promise((resolve) => setTimeout(resolve, 0)); + flushSync(() => {}); + } +} + +describe("RunnerInspector", () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + window.history.replaceState(null, "", "/"); + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + accessMock.mockResolvedValue({ + source: "local_implicit", + isInstanceAdmin: false, + }); + eventsMock.mockResolvedValue([]); + traceMock.mockResolvedValue({ trace: null, entries: [] }); + }); + + afterEach(() => { + flushSync(() => root.unmount()); + container.remove(); + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); + + it("keeps canonical inspection available when raw capture was disabled", async () => { + const rerun = vi.fn(); + flushSync(() => + root.render( + , + ), + ); + await flush(); + + expect(container.textContent).toContain( + "Correlate exact provider traffic with every interpretation stage", + ); + expect(container.textContent).toContain( + "Raw provider capture was off for this run.", + ); + const rerunButton = Array.from(container.querySelectorAll("button")).find( + (button) => button.textContent?.includes("Re-run with provider trace"), + ); + flushSync(() => rerunButton?.click()); + expect(rerun).toHaveBeenCalledOnce(); + }); + + it("shows redacted frames by default and warns before exact reveal", async () => { + traceMock.mockResolvedValue({ + trace: { + id: "trace-1", + runId: "run-1", + companyId: "company-1", + status: "complete", + provider: "codex", + frameCount: 1, + byteCount: 31, + digest: `sha256:${"a".repeat(64)}`, + reason: null, + requestedBy: "local-admin", + createdAt: "2026-08-22T12:00:00.000Z", + expiresAt: "2026-08-23T12:00:00.000Z", + deletedAt: null, + schema: "paperclip.provider_trace_metadata.v1", + }, + entries: [ + { + kind: "frame", + frameId: 1, + direction: "provider_to_client", + byteLength: 31, + parsed: { method: "item/completed", authorization: "[withheld]" }, + withheldPaths: ["authorization"], + }, + ], + }); + revealMock.mockResolvedValue({ + schema: "paperclip.provider_trace_frame.v1", + frameId: 1, + timestamp: "1", + direction: "provider_to_client", + transport: "stdio_jsonl", + provider: "codex", + byteLength: 31, + digest: `sha256:${"b".repeat(64)}`, + rawBase64: btoa(JSON.stringify({ method: "item/completed" })), + }); + const confirm = vi.spyOn(window, "confirm").mockReturnValue(true); + + flushSync(() => + root.render( + , + ), + ); + await flush(); + + expect(container.textContent).toContain("Withheld paths: authorization"); + expect(container.textContent).not.toContain("rawBase64"); + const reveal = Array.from(container.querySelectorAll("button")).find( + (button) => button.textContent?.includes("Reveal exact frame"), + ); + flushSync(() => reveal?.click()); + await flush(); + + expect(confirm).toHaveBeenCalledWith( + expect.stringContaining("may contain prompts"), + ); + expect(revealMock).toHaveBeenCalledWith("run-1", 1); + }); + + it("correlates a Codex web search through every stage to canonical PRP and presentation", async () => { + eventsMock.mockResolvedValue([ + { + id: 91, + companyId: "company-1", + runId: "run-1", + agentId: "agent-1", + seq: 42, + eventType: "research.completed", + stream: "stdout", + level: "info", + color: null, + message: null, + payload: { + prpEvent: { + eventType: "research.completed", + sourceEventId: "event_runner_000001", + sourceSequence: 42, + payload: { query: "best bbq sauce", status: "completed" }, + }, + }, + createdAt: "2026-08-22T12:00:01.000Z", + }, + ]); + traceMock.mockResolvedValue({ + trace: { + id: "trace-1", + runId: "run-1", + companyId: "company-1", + status: "complete", + provider: "codex", + frameCount: 1, + byteCount: 512, + digest: `sha256:${"a".repeat(64)}`, + reason: null, + requestedBy: "local-admin", + createdAt: "2026-08-22T12:00:00.000Z", + expiresAt: "2026-08-23T12:00:00.000Z", + deletedAt: null, + schema: "paperclip.provider_trace_metadata.v1", + }, + entries: [ + { + kind: "frame", + frameId: 27, + timestamp: "1787400001000", + direction: "provider_to_client", + byteLength: 512, + digest: `sha256:${"b".repeat(64)}`, + parsed: { + method: "item/completed", + params: { + item: { + id: "search-1", + type: "webSearch", + query: "best bbq sauce", + results: [{ title: "Sauce guide", url: "https://example.com" }], + }, + }, + }, + withheldPaths: [], + }, + { + kind: "interpretation", + frameId: 27, + debugChannel: "rust_native", + debugSequence: 1, + stage: "rust_jsonrpc_parse", + ruleId: "codex.notification", + disposition: "mapped", + emittedEventIds: [], + droppedFields: [], + reason: "Parsed Codex notification", + }, + { + kind: "interpretation", + frameId: 27, + debugChannel: "rust_native", + debugSequence: 2, + stage: "rust_durable_normalization", + ruleId: "provider.notification.known", + disposition: "mapped", + emittedEventIds: ["event_runner_000001"], + droppedFields: ["params.item.results"], + fieldMappings: [ + { + inputPath: "params.item.query", + outputPath: "payload.query", + action: "copied", + reason: "Preserved the search query", + }, + { + inputPath: "params.item.results", + action: "dropped", + reason: "Raw provider results are not part of this semantic event", + }, + ], + reason: "Normalized provider web search", + }, + { + kind: "interpretation", + frameId: 27, + debugChannel: "typescript_runnerd_rehydration", + debugSequence: 1, + stage: "typescript_runnerd_rehydration", + ruleId: "runnerd.rehydrate.research.completed", + disposition: "mapped", + emittedEventIds: ["event_runner_000001"], + droppedFields: [], + reason: "Rehydrated canonical event", + }, + { + kind: "interpretation", + frameId: 27, + debugChannel: "typescript_runnerd_rehydration", + debugSequence: 2, + stage: "typescript_codex_driver_normalization", + ruleId: "codex_driver.normalize.item/completed", + disposition: "mapped", + emittedEventIds: ["event_runner_000001"], + droppedFields: [], + reason: "Emitted canonical PRP event", + }, + ], + }); + + flushSync(() => + root.render( + , + ), + ); + await flush(); + + expect(container.textContent).toContain("webSearch"); + expect(container.textContent).toContain("rust_jsonrpc_parse"); + expect(container.textContent).toContain("rust_durable_normalization"); + expect(container.textContent).toContain("typescript_runnerd_rehydration"); + expect(container.textContent).toContain("typescript_codex_driver_normalization"); + expect(container.textContent).toContain("params.item.results"); + expect(container.textContent).toContain("payload.query"); + expect(container.textContent).toContain("research.completed"); + expect(container.textContent).toContain("Production surface preview"); + }); +}); diff --git a/ui/src/components/RunnerInspector.tsx b/ui/src/components/RunnerInspector.tsx new file mode 100644 index 00000000000..bfbddec50a5 --- /dev/null +++ b/ui/src/components/RunnerInspector.tsx @@ -0,0 +1,979 @@ +import { useEffect, useMemo, useState } from "react"; +import type { + HeartbeatRunEvent, + ProviderTraceFieldMapping, + ProviderTraceFrame, +} from "@paperclipai/shared"; +import { + ArrowRight, + Braces, + Check, + ChevronDown, + ChevronRight, + CircleOff, + Copy, + Download, + Eye, + EyeOff, + FileJson2, + Layers3, + RefreshCw, + Search, + ShieldAlert, + Trash2, +} from "lucide-react"; +import { heartbeatsApi, type ProviderTraceInspection } from "@/api/heartbeats"; +import { accessApi } from "@/api/access"; +import { parsePaperclipRunnerStdoutLine } from "@/adapters/paperclip-runner"; +import { TaskChatProtocolCard } from "@/components/task-chat/TaskChatProtocolCard"; +import type { TaskChatProtocolItem } from "@/components/task-chat/task-chat-model"; +import { transcriptToTaskChatItems } from "@/components/task-chat/transcript-adapter"; +import { TASK_PROTOCOL_EVENT_SURFACE_REGISTRY } from "@/components/task-chat/task-protocol-surfaces"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { cn } from "@/lib/utils"; +import { copyTextToClipboard } from "@/lib/clipboard"; + +type TraceEntry = Record; +type InspectorView = "overview" | "pipeline" | "trace"; + +type TraceOperation = { + key: string; + frames: TraceEntry[]; + interpretations: TraceEntry[]; + events: HeartbeatRunEvent[]; + title: string; + subtitle: string; + itemType: string; + nativeMethods: string[]; + directions: string[]; + dispositions: string[]; + prpTypes: string[]; + visible: boolean; + timestamp: number; +}; + +const VIEW_OPTIONS: Array<{ + value: InspectorView; + label: string; + icon: typeof Layers3; +}> = [ + { value: "overview", label: "Overview", icon: Layers3 }, + { value: "pipeline", label: "Pipeline", icon: ArrowRight }, + { value: "trace", label: "Exact trace", icon: FileJson2 }, +]; + +function record(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +} + +function text(value: unknown): string { + return typeof value === "string" ? value : ""; +} + +function scalar(value: unknown): string { + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return ""; +} + +function unique(values: string[]) { + return [...new Set(values.filter(Boolean))]; +} + +function formatBytes(value: number) { + if (value < 1_024) return `${value} B`; + if (value < 1_024 * 1_024) return `${(value / 1_024).toFixed(1)} KiB`; + return `${(value / (1_024 * 1_024)).toFixed(1)} MiB`; +} + +function downloadBlob(blob: Blob, filename: string) { + const href = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = href; + anchor.download = filename; + anchor.click(); + URL.revokeObjectURL(href); +} + +function decodeExactFrame(rawBase64: string): unknown { + const decoded = atob(rawBase64); + try { + return JSON.parse(decoded); + } catch { + return decoded; + } +} + +function statusVariant(status: string | undefined) { + if (status === "complete") return "secondary" as const; + if (status === "incomplete" || status === "truncated") + return "destructive" as const; + return "outline" as const; +} + +function traceBadgeLabel(status: string, expiresAt: string | Date) { + if (status === "capturing") return "Raw trace enabled"; + if (status === "incomplete") return "Incomplete"; + if (status === "truncated") return "Truncated"; + if (status === "deleted") return "Deleted"; + if (status === "expired") return "Expired"; + const remainingMs = new Date(expiresAt).getTime() - Date.now(); + if (!Number.isFinite(remainingMs) || remainingMs <= 0) return "Expired"; + const hours = Math.max(1, Math.ceil(remainingMs / (60 * 60 * 1_000))); + return `Expires in ${hours}h`; +} + +function frameParsed(entry: TraceEntry) { + return record(entry.parsed); +} + +function frameMethod(entry: TraceEntry) { + const parsed = frameParsed(entry); + return text(parsed.method) || text(record(parsed.params).method); +} + +function frameItem(entry: TraceEntry) { + const parsed = frameParsed(entry); + const params = record(parsed.params); + return record(params.item ?? parsed.item); +} + +function frameItemType(entry: TraceEntry) { + const parsed = frameParsed(entry); + const params = record(parsed.params); + return text(frameItem(entry).type) || text(params.itemType) || text(parsed.type); +} + +function eventPrp(event: HeartbeatRunEvent) { + const payload = record(event.payload); + const prpEvent = record(payload.prpEvent); + return Object.keys(prpEvent).length > 0 ? prpEvent : payload; +} + +function eventSourceId(event: HeartbeatRunEvent) { + return text(eventPrp(event).sourceEventId); +} + +function eventTimestamp(event: HeartbeatRunEvent) { + const value = new Date(event.createdAt).getTime(); + return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER; +} + +function frameTimestamp(frame: TraceEntry) { + const raw = scalar(frame.timestamp); + const numeric = Number(raw); + if (Number.isFinite(numeric)) { + return numeric < 10_000_000_000 ? numeric * 1_000 : numeric; + } + const parsed = Date.parse(raw); + return Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER; +} + +function visibilityDecision(event: HeartbeatRunEvent) { + const registration = TASK_PROTOCOL_EVENT_SURFACE_REGISTRY[event.eventType]; + if (!registration) { + return { + visible: false, + surface: "run_debug", + container: "Runner Inspector", + state: "hidden", + action: "none", + reasonCode: "presentation_surface_unregistered", + reason: "No production surface registration exists for this event type.", + }; + } + return { + visible: registration.disposition !== "debug-only", + surface: registration.surface, + container: + registration.disposition === "inline" + ? "primary run turn" + : "collapsed activity", + state: registration.disposition, + action: + registration.disposition === "inline" + ? "render" + : registration.disposition === "folded" + ? "fold" + : "operator only", + reasonCode: `presentation_${registration.disposition}`, + reason: registration.rationale, + }; +} + +function interpretationEventIds(entry: TraceEntry) { + return Array.isArray(entry.emittedEventIds) + ? entry.emittedEventIds.map(String) + : []; +} + +function frameCorrelationTokens(frame: TraceEntry, interpretations: TraceEntry[]) { + const parsed = frameParsed(frame); + const params = record(parsed.params); + const item = frameItem(frame); + const tokens = interpretations.flatMap(interpretationEventIds).map((id) => `event:${id}`); + const rpcId = scalar(parsed.id); + if (rpcId) tokens.push(`rpc:${rpcId}`); + for (const id of [item.id, params.itemId, parsed.itemId]) { + const value = scalar(id); + if (value) tokens.push(`item:${value}`); + } + return unique(tokens); +} + +function buildOperations( + frames: TraceEntry[], + interpretations: TraceEntry[], + events: HeartbeatRunEvent[], +): TraceOperation[] { + const sortedFrames = [...frames].sort( + (left, right) => Number(left.frameId) - Number(right.frameId), + ); + const frameById = new Map(sortedFrames.map((frame) => [Number(frame.frameId), frame])); + const parent = new Map([...frameById.keys()].map((id) => [id, id])); + const find = (id: number): number => { + const current = parent.get(id) ?? id; + if (current === id) return id; + const root = find(current); + parent.set(id, root); + return root; + }; + const union = (left: number, right: number) => { + const leftRoot = find(left); + const rightRoot = find(right); + if (leftRoot !== rightRoot) parent.set(rightRoot, leftRoot); + }; + const tokenOwner = new Map(); + for (const frame of sortedFrames) { + const frameId = Number(frame.frameId); + const stages = interpretations.filter((entry) => Number(entry.frameId) === frameId); + for (const token of frameCorrelationTokens(frame, stages)) { + const owner = tokenOwner.get(token); + if (owner === undefined) tokenOwner.set(token, frameId); + else union(owner, frameId); + } + } + + const groupedFrames = new Map(); + for (const frame of sortedFrames) { + const root = find(Number(frame.frameId)); + groupedFrames.set(root, [...(groupedFrames.get(root) ?? []), frame]); + } + const claimedEvents = new Set(); + const operations: TraceOperation[] = []; + for (const groupFrames of groupedFrames.values()) { + const frameIds = new Set(groupFrames.map((frame) => Number(frame.frameId))); + const stages = interpretations.filter((entry) => frameIds.has(Number(entry.frameId))); + const emittedIds = new Set(stages.flatMap(interpretationEventIds)); + const groupEvents = events.filter((event) => emittedIds.has(eventSourceId(event))); + groupEvents.forEach((event) => claimedEvents.add(event.id)); + const methods = unique(groupFrames.map(frameMethod)); + const itemTypes = unique(groupFrames.map(frameItemType)); + const title = itemTypes.at(-1) || methods.at(-1) || text(groupFrames[0]?.direction) || "Provider frame"; + const firstFrame = groupFrames[0]; + const lastFrame = groupFrames.at(-1); + const range = firstFrame === lastFrame + ? `frame ${firstFrame?.frameId}` + : `frames ${firstFrame?.frameId}–${lastFrame?.frameId}`; + operations.push({ + key: `frames:${firstFrame?.frameId}`, + frames: groupFrames, + interpretations: stages, + events: groupEvents, + title, + subtitle: `${range} · ${groupEvents.length} PRP event${groupEvents.length === 1 ? "" : "s"}`, + itemType: itemTypes.at(-1) ?? "", + nativeMethods: methods, + directions: unique(groupFrames.map((frame) => text(frame.direction))), + dispositions: unique(stages.map((entry) => text(entry.disposition))), + prpTypes: unique(groupEvents.map((event) => event.eventType)), + visible: groupEvents.some((event) => visibilityDecision(event).visible), + timestamp: Math.min(...groupFrames.map(frameTimestamp)), + }); + } + for (const event of events) { + if (claimedEvents.has(event.id)) continue; + const decision = visibilityDecision(event); + operations.push({ + key: `event:${event.id}`, + frames: [], + interpretations: [], + events: [event], + title: event.eventType, + subtitle: `PRP ${eventSourceId(event) || `event ${event.seq}`} · no raw correlation`, + itemType: "", + nativeMethods: [], + directions: [], + dispositions: [], + prpTypes: [event.eventType], + visible: decision.visible, + timestamp: eventTimestamp(event), + }); + } + return operations.sort((left, right) => left.timestamp - right.timestamp); +} + +async function loadAllRunEvents(runId: string) { + const events: HeartbeatRunEvent[] = []; + let afterSeq = 0; + for (;;) { + const page = await heartbeatsApi.events(runId, afterSeq, 1_000); + events.push(...page); + if (page.length < 1_000) return events; + const nextSeq = page.at(-1)?.seq ?? afterSeq; + if (nextSeq <= afterSeq) return events; + afterSeq = nextSeq; + } +} + +function jsonMatches(value: unknown, query: string): boolean { + if (!query) return true; + try { + return JSON.stringify(value).toLowerCase().includes(query.toLowerCase()); + } catch { + return String(value).toLowerCase().includes(query.toLowerCase()); + } +} + +function JsonPrimitive({ value }: { value: unknown }) { + if (typeof value === "string") return "{value}"; + if (typeof value === "number") return {value}; + if (typeof value === "boolean") return {String(value)}; + if (value === null) return null; + return {String(value)}; +} + +function JsonNode({ + label, + value, + path, + depth, + query, +}: { + label?: string; + value: unknown; + path: string; + depth: number; + query: string; +}) { + const expandable = value !== null && typeof value === "object"; + const entries = Array.isArray(value) + ? value.map((child, index) => [String(index), child] as const) + : Object.entries(record(value)); + const large = entries.length > 8; + const [expanded, setExpanded] = useState(depth < 1 && !large); + const forcedOpen = Boolean(query) && jsonMatches(value, query); + const isOpen = expandable && (expanded || forcedOpen); + const visibleEntries = query + ? entries.filter(([key, child]) => + key.toLowerCase().includes(query.toLowerCase()) || jsonMatches(child, query), + ) + : entries; + const copyValue = () => { + const serialized = typeof value === "string" ? value : JSON.stringify(value, null, 2); + void copyTextToClipboard(serialized ?? String(value)); + }; + return ( +
0 && "border-l border-border/60 pl-3")}> +
+ {expandable ? ( + + ) : ( + + )} + {label !== undefined ? {label}: : null} + {expandable ? ( + + {Array.isArray(value) ? `[${entries.length}]` : `{${entries.length}}`} + + ) : ( + + )} + + + + +
+ {isOpen ? ( +
+ {visibleEntries.map(([key, child]) => ( + + ))} + {query && visibleEntries.length === 0 ? ( +

No fields match.

+ ) : null} +
+ ) : null} +
+ ); +} + +function JsonExplorer({ value, label = "Search this JSON" }: { value: unknown; label?: string }) { + const [query, setQuery] = useState(""); + return ( +
+
+ + setQuery(event.target.value)} + placeholder={label} + className="h-8 border-0 bg-transparent pl-8 text-xs shadow-none focus-visible:ring-0" + /> +
+
+ +
+
+ ); +} + +function fieldMappings(entry: TraceEntry): ProviderTraceFieldMapping[] { + const explicit = Array.isArray(entry.fieldMappings) + ? entry.fieldMappings + .map(record) + .filter((mapping) => text(mapping.action)) + .map((mapping) => ({ + inputPath: text(mapping.inputPath) || undefined, + outputPath: text(mapping.outputPath) || undefined, + action: text(mapping.action) as ProviderTraceFieldMapping["action"], + reason: text(mapping.reason) || undefined, + })) + : []; + const knownDrops = new Set(explicit.filter((mapping) => mapping.action === "dropped").map((mapping) => mapping.inputPath)); + const legacy = Array.isArray(entry.droppedFields) + ? entry.droppedFields + .map(String) + .filter((path) => !knownDrops.has(path)) + .map((path) => ({ + inputPath: path, + action: "dropped" as const, + reason: text(entry.reason) || "Field was not carried into the next stage", + })) + : []; + return [...explicit, ...legacy]; +} + +function mappingTone(action: ProviderTraceFieldMapping["action"]) { + if (action === "dropped" || action === "redacted") return "border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300"; + if (action === "derived") return "border-violet-500/30 bg-violet-500/10 text-violet-700 dark:text-violet-300"; + return "border-cyan-500/30 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300"; +} + +function InterpretationStage({ entry, last }: { entry: TraceEntry; last: boolean }) { + const mappings = fieldMappings(entry); + return ( +
+
+ {!last ? : null} + +
+
+
+ {text(entry.stage)} + {text(entry.disposition)} + {text(entry.ruleId)} +
+

{text(entry.reason)}

+ {mappings.length > 0 ? ( +
+ + + + + + + + + + + {mappings.map((mapping, index) => ( + + + + + + + ))} + +
ActionProvider pathOutput pathReason
+ {mapping.action} + {mapping.inputPath ?? "—"}{mapping.outputPath ?? "—"}{mapping.reason ?? "—"}
+
+ ) : ( +

No field-level mapping was recorded at this stage.

+ )} +
+
+ ); +} + +function typedPrpFields(event: HeartbeatRunEvent) { + const prp = eventPrp(event); + const payload = record(prp.payload); + const item = record(payload.item); + return [ + ["Event type", event.eventType], + ["Source event", text(prp.sourceEventId)], + ["Sequence", scalar(prp.sourceSequence) || String(event.seq)], + ["Item type", text(item.type) || text(payload.kind)], + ["Status", text(payload.status) || text(item.status)], + ["Query", text(payload.query) || text(item.query)], + ].filter(([, value]) => Boolean(value)); +} + +function ProductionSurfacePreview({ event, runId }: { event: HeartbeatRunEvent; runId: string }) { + const prp = eventPrp(event); + const ts = new Date(event.createdAt).toISOString(); + const entries = parsePaperclipRunnerStdoutLine( + JSON.stringify({ type: "paperclip.prp.event", event: prp }), + ts, + ); + const item = transcriptToTaskChatItems(entries, { + runId, + agentName: "Runner", + running: false, + }).find((candidate): candidate is TaskChatProtocolItem => candidate.kind === "protocol"); + if (item) return ; + const decision = visibilityDecision(event); + const VisibleIcon = decision.visible ? Eye : EyeOff; + return ( +
+
+ +
+

{event.eventType}

+

+ Production surface: {decision.surface} · {decision.action} in {decision.container} +

+
+
+
+ ); +} + +function StatCard({ label, value, detail }: { label: string; value: string | number; detail?: string }) { + return ( +
+

{label}

+

{value}

+ {detail ?

{detail}

: null} +
+ ); +} + +export function RunnerInspector({ + runId, + run, + open, + onOpenChange, + onRerunWithTrace, +}: { + runId: string; + run?: { resultJson: Record | null; status: string } | null; + open: boolean; + onOpenChange: (open: boolean) => void; + onRerunWithTrace?: () => void; +}) { + const [inspection, setInspection] = useState(null); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [view, setView] = useState("pipeline"); + const [selectedKey, setSelectedKey] = useState(null); + const [selectedFrameId, setSelectedFrameId] = useState(null); + const [query, setQuery] = useState(""); + const [direction, setDirection] = useState("all"); + const [nativeMethod, setNativeMethod] = useState("all"); + const [disposition, setDisposition] = useState("all"); + const [prpType, setPrpType] = useState("all"); + const [visibility, setVisibility] = useState("all"); + const [revealed, setRevealed] = useState>({}); + const [canInspectRaw, setCanInspectRaw] = useState(null); + + useEffect(() => { + if (!open) return; + const params = new URLSearchParams(window.location.search); + const requestedView = params.get("traceView"); + if (requestedView === "overview" || requestedView === "pipeline" || requestedView === "trace") setView(requestedView); + const requestedOperation = params.get("traceOperation"); + if (requestedOperation) setSelectedKey(requestedOperation); + }, [open]); + + useEffect(() => { + if (!open) return; + let active = true; + setLoading(true); + setError(null); + Promise.all([accessApi.getCurrentBoardAccess(), loadAllRunEvents(runId)]) + .then(async ([boardAccess, nextEvents]) => { + if (!active) return; + const canRaw = boardAccess.source === "local_implicit" || boardAccess.isInstanceAdmin; + setCanInspectRaw(canRaw); + const trace = canRaw + ? await heartbeatsApi.providerTrace(runId) + : ({ trace: null, entries: [] } satisfies ProviderTraceInspection); + if (!active) return; + setInspection(trace); + setEvents(nextEvents); + }) + .catch((cause) => { + if (!active) return; + setError(cause instanceof Error ? cause.message : "Runner inspection failed"); + }) + .finally(() => { + if (active) setLoading(false); + }); + return () => { + active = false; + }; + }, [open, runId]); + + const entries = inspection?.entries ?? []; + const frames = useMemo(() => entries.filter((entry) => entry.kind === "frame"), [entries]); + const interpretations = useMemo(() => entries.filter((entry) => entry.kind === "interpretation"), [entries]); + const capturedProviders = unique(frames.map((frame) => text(frame.provider))); + const operations = useMemo( + () => buildOperations(frames, interpretations, events), + [events, frames, interpretations], + ); + const filteredOperations = useMemo( + () => operations.filter((operation) => { + if (query.trim() && !jsonMatches(operation, query.trim())) return false; + if (direction !== "all" && !operation.directions.includes(direction)) return false; + if (nativeMethod !== "all" && !operation.nativeMethods.includes(nativeMethod)) return false; + if (disposition !== "all" && !operation.dispositions.includes(disposition)) return false; + if (prpType !== "all" && !operation.prpTypes.includes(prpType)) return false; + if (visibility === "visible" && !operation.visible) return false; + if (visibility === "hidden" && operation.visible) return false; + return true; + }), + [direction, disposition, nativeMethod, operations, prpType, query, visibility], + ); + + useEffect(() => { + if (!filteredOperations.length) return; + if (!selectedKey || !filteredOperations.some((operation) => operation.key === selectedKey)) { + const preferred = + filteredOperations.find( + (operation) => operation.itemType && operation.events.length > 0, + ) ?? + filteredOperations.find( + (operation) => operation.frames.length > 0 && operation.events.length > 0, + ) ?? + filteredOperations[0]!; + setSelectedKey(preferred.key); + } + }, [filteredOperations, selectedKey]); + + const selectedOperation = filteredOperations.find((operation) => operation.key === selectedKey) ?? filteredOperations[0] ?? null; + useEffect(() => { + if (!selectedOperation) return; + if (!selectedOperation.frames.some((frame) => Number(frame.frameId) === selectedFrameId)) { + const preferred = [...selectedOperation.frames].reverse().find((frame) => frameMethod(frame).includes("completed")) ?? selectedOperation.frames.at(-1); + setSelectedFrameId(preferred ? Number(preferred.frameId) : null); + } + }, [selectedFrameId, selectedOperation]); + + useEffect(() => { + if (!open || !selectedOperation) return; + const params = new URLSearchParams(window.location.search); + params.set("inspectRun", runId); + params.set("traceView", view); + params.set("traceOperation", selectedOperation.key); + const next = `${window.location.pathname}?${params.toString()}${window.location.hash}`; + window.history.replaceState(window.history.state, "", next); + }, [open, runId, selectedOperation, view]); + + const selectedFrame = selectedOperation?.frames.find((frame) => Number(frame.frameId) === selectedFrameId) ?? selectedOperation?.frames.at(-1) ?? null; + const selectedInterpretations = selectedFrame + ? selectedOperation?.interpretations.filter((entry) => Number(entry.frameId) === Number(selectedFrame.frameId)) ?? [] + : selectedOperation?.interpretations ?? []; + const selectedEvents = selectedOperation?.events ?? []; + const presentationDecision = record(record(run?.resultJson).presentationDecision); + const visibleEventCount = events.filter((event) => visibilityDecision(event).visible).length; + const ignoredCount = interpretations.filter((entry) => entry.disposition === "ignored").length; + const dispositionCounts = interpretations.reduce>((counts, entry) => { + const key = text(entry.disposition) || "unknown"; + counts[key] = (counts[key] ?? 0) + 1; + return counts; + }, {}); + + const selectOperation = (operation: TraceOperation) => { + setSelectedKey(operation.key); + setView("pipeline"); + }; + + const reveal = async (frameId: number) => { + if (!window.confirm("This exact provider frame may contain prompts, tool arguments, secrets, or reasoning. Reveal it now?")) return; + const frame = await heartbeatsApi.revealProviderTraceFrame(runId, frameId); + setRevealed((current) => ({ ...current, [frameId]: frame })); + }; + + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen) { + const params = new URLSearchParams(window.location.search); + params.delete("inspectRun"); + params.delete("traceView"); + params.delete("traceOperation"); + const suffix = params.toString(); + window.history.replaceState( + window.history.state, + "", + `${window.location.pathname}${suffix ? `?${suffix}` : ""}${window.location.hash}`, + ); + } + onOpenChange(nextOpen); + }; + + return ( + + + +
+ Runner Inspector + {inspection?.trace ? ( + <> + + {traceBadgeLabel(inspection.trace.status, inspection.trace.expiresAt)} + + + {capturedProviders.join(", ") || inspection.trace.provider} + + + ) : null} +
+ + Correlate exact provider traffic with every interpretation stage, canonical PRP event, and production surface. + +
+ {VIEW_OPTIONS.map((option) => { + const Icon = option.icon; + return ( + + ); + })} +
+
+ +
+ {error ?
{error}
: null} + {!loading && !error && canInspectRaw === false ? ( +
+

Raw provider traces require an instance administrator.

+

Canonical PRP events and presentation decisions remain inspectable below.

+
+ ) : null} + {!loading && !error && canInspectRaw !== false && !inspection?.trace ? ( +
+
+

Raw provider capture was off for this run.

+

Canonical PRP events and persisted presentation decisions remain available below.

+
+ {onRerunWithTrace && canInspectRaw ? ( + + ) : null} +
+ ) : null} + + {view === "overview" ? ( + +
+ {loading ?

Loading run pipeline…

: null} +
+ + + + + + +
+
+
+

Interpretation outcomes

+
+ {Object.entries(dispositionCounts).length ? Object.entries(dispositionCounts).map(([label, count]) => ( +
+ {label} +
+ {count} +
+ )) :

No interpretation records were persisted.

} +
+
+
+

Recent correlated operations

+
+ {operations.slice(-6).reverse().map((operation) => ( + + ))} +
+
+
+
+

Sensitive debug data

Parsed frames are redacted on the server. Exact reveals and downloads are administrator-only, warned, audited, and automatically expire.

+
+
+
+ ) : null} + + {view === "pipeline" ? ( + <> +
+
setQuery(event.target.value)} placeholder="Search operations, fields, and events" />
+ + + + + +
+
+ +
+ {loading ?

Loading run pipeline…

: null} + {!loading && filteredOperations.length === 0 ?

No operations match these filters.

: null} + {filteredOperations.map((operation) => ( + + ))} +
+
+ + {selectedOperation ? ( +
+
+ {selectedOperation.title}{selectedOperation.frames.length} raw frame{selectedOperation.frames.length === 1 ? "" : "s"}{selectedOperation.interpretations.length} mapping stage{selectedOperation.interpretations.length === 1 ? "" : "s"}{selectedEvents.length} PRP event{selectedEvents.length === 1 ? "" : "s"} +
+
+
1

Provider frames

server-redacted by default
+ {selectedOperation.frames.length > 1 ?
{selectedOperation.frames.map((frame) => )}
: null} + {selectedFrame ? ( +
+
frame {String(selectedFrame.frameId)}{text(selectedFrame.direction).replaceAll("_", " ")}{formatBytes(Number(selectedFrame.byteLength) || 0)}{text(selectedFrame.digest)}
+ + {Array.isArray(selectedFrame.withheldPaths) && selectedFrame.withheldPaths.length > 0 ?
Withheld paths: {selectedFrame.withheldPaths.join(", ")}
: null} + {canInspectRaw ? : null} + {revealed[Number(selectedFrame.frameId)] ?

Exact unredacted frame

: null} +
+ ) :

This PRP event has no recoverable raw frame.

} +
+
+
2

Interpretation stages

+ {selectedInterpretations.length ? selectedInterpretations.map((entry, index) => ) :

No interpretation stage was recorded for this frame.

} +
+
+
3

Canonical PRP events

+ {selectedEvents.length ? selectedEvents.map((event) => ( +
+
{event.eventType}seq {event.seq}
+
{typedPrpFields(event).map(([label, value]) =>
{label}
{value}
)}
+
Canonical event JSON
+
+ )) :

This provider operation emitted no canonical PRP events.

} +
+
+
4

Production presentation

+ {selectedEvents.length ? selectedEvents.map((event) => { + const decision = visibilityDecision(event); + return
Visible
{decision.visible ? : }{decision.visible ? "yes" : "no"}
Surface
{decision.surface}
Container
{decision.container}
State
{decision.state}
Action
{decision.action}
Reason
{decision.reason}
Reason code
{decision.reasonCode}

Production surface preview

; + }) :

No presentation surface was emitted for this operation.

} + {Object.keys(presentationDecision).length > 0 ?
Resolved final response decision
: null} +
+
+ ) :
Select a correlated operation to inspect its pipeline.
} +
+
+ + ) : null} + + {view === "trace" ? ( +
+
{frames.length ? [...frames].sort((left, right) => Number(left.frameId) - Number(right.frameId)).map((frame) => ) :

No raw frames were captured for this run.

}
+
{selectedFrame ? <>

Exact trace frame #{String(selectedFrame.frameId)}

{frameMethod(selectedFrame) || text(selectedFrame.direction)}
{canInspectRaw ? : null}{revealed[Number(selectedFrame.frameId)] ? : null} :

Select a frame from the chronological trace.

}
+
+ ) : null} + + {inspection?.trace ? ( +
+

{inspection.trace.frameCount} frames · {formatBytes(inspection.trace.byteCount)} · expires {new Date(inspection.trace.expiresAt).toLocaleString()}

+
+
+ ) : null} +
+
+
+ ); +} diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index fb621c1bf8a..c57fdc9c0d2 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -6,8 +6,8 @@ import { flushSync } from "react-dom"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ThemeProvider } from "@/context/ThemeContext"; -import type { IssueQueuedCommentQueue } from "@paperclipai/shared"; import { TaskChatThread } from "./TaskChatThread"; +import type { IssueQueuedCommentQueue } from "@paperclipai/shared"; const transcriptState = vi.hoisted(() => ({ transcriptByRun: new Map() })); const sidebarState = vi.hoisted(() => ({ isMobile: false })); @@ -117,6 +117,49 @@ describe("TaskChatThread draft pass-through", () => { }); }); +describe("TaskChatThread causal comment ordering", () => { + it("places a queued input where its consuming run starts", () => { + const comment = ( + id: string, + body: string, + createdAt: string, + authorType: "user" | "agent", + extra: Record = {}, + ) => ({ + id, + companyId: "company-1", + issueId: "issue-1", + authorType, + authorAgentId: authorType === "agent" ? "agent-1" : null, + authorUserId: authorType === "user" ? "user-1" : null, + body, + presentation: null, + metadata: null, + createdAt: new Date(createdAt), + updatedAt: new Date(createdAt), + ...extra, + }); + render( {}} + />); + + const text = container.textContent ?? ""; + expect(text.indexOf("Initial recipe request")).toBeLessThan(text.indexOf("Pork shoulder recipe")); + expect(text.indexOf("Pork shoulder recipe")).toBeLessThan(text.indexOf("What about BBQ sauce?")); + expect(text.indexOf("What about BBQ sauce?")).toBeLessThan(text.indexOf("BBQ sauce recipe")); + }); +}); + describe("TaskChatThread native-runner failures", () => { it("replaces an empty failed transcript with actionable retry feedback and keeps the composer enabled", () => { render( diff --git a/ui/src/components/TaskChatThread.tsx b/ui/src/components/TaskChatThread.tsx index 3e50447593e..22f77dd75a3 100644 --- a/ui/src/components/TaskChatThread.tsx +++ b/ui/src/components/TaskChatThread.tsx @@ -1,4 +1,12 @@ -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentProps } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + type ComponentProps, +} from "react"; import { IssueChatThread } from "@/components/IssueChatThread"; import { useLiveRunTranscripts, @@ -40,7 +48,10 @@ import { import { shouldHideInteractionCard } from "@/lib/issue-thread-interactions"; import { TaskChatBubbleActions } from "@/components/task-chat/TaskChatBubbleActions"; import type { FeedbackVoteValue } from "@paperclipai/shared"; -import { TaskChatThreadView, taskChatContentKey } from "@/components/task-chat/TaskChatThreadView"; +import { + TaskChatThreadView, + taskChatContentKey, +} from "@/components/task-chat/TaskChatThreadView"; import { TaskChatComposer } from "@/components/task-chat/TaskChatComposer"; import { TaskChatQueuedMessages } from "@/components/task-chat/TaskChatQueuedMessages"; import { useWindowAutoFollow } from "@/components/task-chat/useWindowAutoFollow"; @@ -102,8 +113,11 @@ function isRunnerResponseComment(params: { if (comment.runId === runId) return true; if (comment.runId || !finalText) return false; const commentAtMs = toMs(comment.createdAt); - if (runStartedAtMs != null && commentAtMs < runStartedAtMs - 5_000) return false; - return normalizedMessageText(comment.body) === normalizedMessageText(finalText); + if (runStartedAtMs != null && commentAtMs < runStartedAtMs - 5_000) + return false; + return ( + normalizedMessageText(comment.body) === normalizedMessageText(finalText) + ); } // PAP-462 B4: backstop for how long the just-settled run's transcript stays @@ -112,6 +126,45 @@ function isRunnerResponseComment(params: { // well within this as soon as the settled turn/comment lands. const SETTLING_TAIL_MAX_MS = 15_000; const EMPTY_LIVE_ISSUE_IDS: ReadonlySet = new Set(); +const LEGACY_WITHHELD_RUN_COMMENT = + "Run completed. Agent did not post a summary comment this run (transcript withheld — see run log)."; + +function acceptedSemanticResultSummary(value: unknown): string | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const resultJson = value as Record; + const semanticResult = resultJson.semanticResult; + const candidates = [ + resultJson.nativeResult, + resultJson.acceptedResult, + semanticResult && + typeof semanticResult === "object" && + !Array.isArray(semanticResult) + ? (semanticResult as Record).result + : null, + ]; + for (const candidate of candidates) { + if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) + continue; + const result = candidate as Record; + if (result.schema !== "paperclip.run_result.v1") continue; + if (typeof result.summary === "string" && result.summary.trim()) + return result.summary; + } + return null; +} + +function resolvedWithoutUserFacingResponse(value: unknown): boolean { + if (!value || typeof value !== "object" || Array.isArray(value)) return false; + const decision = (value as Record).presentationDecision; + return Boolean( + decision && + typeof decision === "object" && + !Array.isArray(decision) && + (decision as Record).schema === + "paperclip.run_presentation_decision.v1" && + (decision as Record).chosenSource === "none", + ); +} export type TaskChatThreadProps = ComponentProps; @@ -297,21 +350,33 @@ export function TaskChatThread(props: TaskChatThreadProps) { }, [paperclipQueue, queuedEdit]); const liveWorkLinks = useMemo( - () => issueStatus === "blocked" && blockerAttention?.state === "covered" - ? resolveTaskChatLiveWork(blockedBy, liveIssueIds ?? EMPTY_LIVE_ISSUE_IDS, blockerAttention.terminalBlocker) - : null, - [blockedBy, blockerAttention?.state, blockerAttention?.terminalBlocker, issueStatus, liveIssueIds], + () => + issueStatus === "blocked" && blockerAttention?.state === "covered" + ? resolveTaskChatLiveWork( + blockedBy, + liveIssueIds ?? EMPTY_LIVE_ISSUE_IDS, + blockerAttention.terminalBlocker, + ) + : null, + [ + blockedBy, + blockerAttention?.state, + blockerAttention?.terminalBlocker, + issueStatus, + liveIssueIds, + ], ); const blockerLinks = useMemo( - () => issueStatus === "blocked" && !liveWorkLinks - ? resolveTaskChatBlockers( - blockedBy, - blockerAttention?.terminalBlockerIssueId, - blockerAttention?.directBlockerIssueId, - blockerAttention?.terminalBlocker, - ) - : null, + () => + issueStatus === "blocked" && !liveWorkLinks + ? resolveTaskChatBlockers( + blockedBy, + blockerAttention?.terminalBlockerIssueId, + blockerAttention?.directBlockerIssueId, + blockerAttention?.terminalBlocker, + ) + : null, [ blockedBy, blockerAttention?.directBlockerIssueId, @@ -322,20 +387,21 @@ export function TaskChatThread(props: TaskChatThreadProps) { ], ); - const threadHeaderWithBlockers = threadHeader || blockerLinks || liveWorkLinks ? ( - <> - {threadHeader} - {liveWorkLinks ? ( - - ) : blockerLinks ? ( - - ) : null} - - ) : undefined; + const threadHeaderWithBlockers = + threadHeader || blockerLinks || liveWorkLinks ? ( + <> + {threadHeader} + {liveWorkLinks ? ( + + ) : blockerLinks ? ( + + ) : null} + + ) : undefined; const bottomBlockerLinks = liveWorkLinks ? ( @@ -348,19 +414,45 @@ export function TaskChatThread(props: TaskChatThreadProps) { ) : null; const linkedRunMetaById = useMemo(() => { - const map = new Map[number]>(); + const map = new Map< + string, + NonNullable[number] + >(); for (const run of linkedRuns ?? []) map.set(run.runId, run); return map; }, [linkedRuns]); + // Historical placeholder comments remain untouched in storage. Projection + // prefers the accepted semantic result so runs such as DOT-20 recover their + // real upstream response without a data migration. + const projectedComments = useMemo( + () => + comments.map((comment) => { + if (comment.body !== LEGACY_WITHHELD_RUN_COMMENT || !comment.runId) + return comment; + const summary = acceptedSemanticResultSummary( + linkedRunMetaById.get(comment.runId)?.resultJson, + ); + return summary ? { ...comment, body: summary } : comment; + }), + [comments, linkedRunMetaById], + ); + const commentItems = useMemo( - () => commentsToTaskChatItems(comments, { + () => + commentsToTaskChatItems(projectedComments, { + agentMap, + userLabelMap, + currentUserId, + issueAssigneeAgentId, + }), + [ + projectedComments, agentMap, userLabelMap, currentUserId, issueAssigneeAgentId, - }), - [comments, agentMap, userLabelMap, currentUserId, issueAssigneeAgentId], + ], ); // Every run we might need a transcript for (history + live), deduped by id. @@ -410,12 +502,17 @@ export function TaskChatThread(props: TaskChatThreadProps) { // canonical comment and settled activity are both ready. This state is // established in a layout effect so the live lane never disappears for a // paint between `liveRun` ending and the persistence handoff beginning. - const [settlingRun, setSettlingRun] = useState<{ id: string; startedAtMs: number | null } | null>(null); + const [settlingRun, setSettlingRun] = useState<{ + id: string; + startedAtMs: number | null; + } | null>(null); const prevLiveRunRef = useRef(null); useLayoutEffect(() => { if (liveRun) { prevLiveRunRef.current = liveRun; - setSettlingRun((current) => (current && current.id !== liveRun.id ? null : current)); + setSettlingRun((current) => + current && current.id !== liveRun.id ? null : current, + ); return; } const prev = prevLiveRunRef.current; @@ -423,24 +520,34 @@ export function TaskChatThread(props: TaskChatThreadProps) { if (!prev) return; setSettlingRun({ id: prev.id, - startedAtMs: (prev.startedAt ? toMs(prev.startedAt) : null) ?? toMs(prev.createdAt), + startedAtMs: + (prev.startedAt ? toMs(prev.startedAt) : null) ?? toMs(prev.createdAt), }); }, [liveRun]); - const heldPaperclipRunnerRunId = liveRun?.adapterType === "paperclip_runner" - ? liveRun.id - : settlingRun && runs.find((run) => run.id === settlingRun.id)?.adapterType === "paperclip_runner" - ? settlingRun.id - : null; - const heldPaperclipRunnerStartedAtMs = heldPaperclipRunnerRunId === liveRun?.id - ? (liveRun.startedAt ? toMs(liveRun.startedAt) : null) ?? toMs(liveRun.createdAt) - : settlingRun?.startedAtMs ?? null; + const heldPaperclipRunnerRunId = + liveRun?.adapterType === "paperclip_runner" + ? liveRun.id + : settlingRun && + runs.find((run) => run.id === settlingRun.id)?.adapterType === + "paperclip_runner" + ? settlingRun.id + : null; + const heldPaperclipRunnerStartedAtMs = + heldPaperclipRunnerRunId === liveRun?.id + ? ((liveRun.startedAt ? toMs(liveRun.startedAt) : null) ?? + toMs(liveRun.createdAt)) + : (settlingRun?.startedAtMs ?? null); const heldPaperclipRunnerFinalText = useMemo(() => { if (!heldPaperclipRunnerRunId) return null; const entries = transcriptByRun.get(heldPaperclipRunnerRunId) ?? []; for (let index = entries.length - 1; index >= 0; index -= 1) { const entry = entries[index]; - if (entry.kind === "assistant" && entry.channel === "final" && entry.text.trim()) { + if ( + entry.kind === "assistant" && + entry.channel === "final" && + entry.text.trim() + ) { return entry.text; } } @@ -475,10 +582,17 @@ export function TaskChatThread(props: TaskChatThreadProps) { // legacy buildIssueChatMessages), so plan-mode confirmation/question cards // land where they happened in the conversation. const orderedEntries = useMemo(() => { - const entries: { ms: number; order: number; id: string; item: TaskChatItem }[] = []; + const entries: { + ms: number; + order: number; + id: string; + item: TaskChatItem; + }[] = []; // commentsToTaskChatItems skips deleted comments — mirror its filter so the // two lists stay index-aligned. - const visibleComments = comments.filter((comment) => !comment.deletedAt); + const visibleComments = projectedComments.filter( + (comment) => !comment.deletedAt, + ); visibleComments.forEach((comment, index) => { if (queuedCommentIds.has(comment.id)) return; // The live/settling runner lane already renders this run's final answer. @@ -492,10 +606,20 @@ export function TaskChatThread(props: TaskChatThreadProps) { runStartedAtMs: heldPaperclipRunnerStartedAtMs, finalText: heldPaperclipRunnerFinalText, }) - ) return; + ) + return; const item = commentItems[index]; if (!item) return; - entries.push({ ms: toMs(comment.createdAt), order: 1, id: item.id, item }); + entries.push({ + // A held/queued input belongs where the runner actually consumes it, + // not where the user first submitted it while another turn was live. + ms: comment.conversationAnchorAt + ? toMs(comment.conversationAnchorAt) + : toMs(comment.createdAt), + order: 1 + Math.min(comment.conversationAnchorSequence ?? 0, 999) / 1000, + id: item.id, + item, + }); }); for (const interaction of interactions ?? []) { // Withdrawn/superseded confirmations are retracted calls to action — drop @@ -544,7 +668,11 @@ export function TaskChatThread(props: TaskChatThreadProps) { resourceKind: "document", title: documentDisplayTitle(document), subtitle: `Document · rev ${document.latestRevisionNumber}`, - href: buildDocumentAnnotationHash({ documentKey: document.key, threadId: null, commentId: null }), + href: buildDocumentAnnotationHash({ + documentKey: document.key, + threadId: null, + commentId: null, + }), timestamp: new Date(document.updatedAt).toISOString(), document, }, @@ -563,14 +691,23 @@ export function TaskChatThread(props: TaskChatThreadProps) { surface: "resource", resourceKind: "deliverable", title: workProduct.title, - subtitle: [workProduct.type.replaceAll("_", " "), workProduct.status, workProduct.reviewState !== "none" ? workProduct.reviewState : null].filter(Boolean).join(" · "), + subtitle: [ + workProduct.type.replaceAll("_", " "), + workProduct.status, + workProduct.reviewState !== "none" ? workProduct.reviewState : null, + ] + .filter(Boolean) + .join(" · "), href: safeResourceHref(href), timestamp: new Date(workProduct.createdAt).toISOString(), workProduct, }, }); } - for (const attachment of selectAgentArtifactAttachments(attachments, workProducts)) { + for (const attachment of selectAgentArtifactAttachments( + attachments, + workProducts, + )) { const id = `resource:attachment:${attachment.id}`; entries.push({ ms: toMs(attachment.createdAt), @@ -608,13 +745,32 @@ export function TaskChatThread(props: TaskChatThreadProps) { return entries.sort( (a, b) => a.ms - b.ms || a.order - b.order || a.id.localeCompare(b.id), ); - }, [comments, commentItems, interactions, documents, workProducts, attachments, timelineEvents, linkedRuns, liveRuns, planDocument, heldPaperclipRunnerRunId, heldPaperclipRunnerStartedAtMs, heldPaperclipRunnerFinalText, queuedCommentIds]); + }, [ + comments, + projectedComments, + commentItems, + interactions, + documents, + workProducts, + attachments, + timelineEvents, + linkedRuns, + liveRuns, + planDocument, + heldPaperclipRunnerRunId, + heldPaperclipRunnerStartedAtMs, + heldPaperclipRunnerFinalText, + queuedCommentIds, + ]); // Boolean gate (stable across the host's per-render brief objects) so the // heavy assembly memo doesn't recompute on every parent render. const hasBrief = Boolean(issueBrief); - const { items, settledRunIds } = useMemo<{ items: TaskChatItem[]; settledRunIds: Set }>(() => { + const { items, settledRunIds } = useMemo<{ + items: TaskChatItem[]; + settledRunIds: Set; + }>(() => { // Runs whose settled turn made it into the assembled thread — the live tail // hands off to this as its "the settled turn has rendered" signal (PAP-462 // B4), so the transcript stays mounted through the settle gap without ever @@ -626,7 +782,11 @@ export function TaskChatThread(props: TaskChatThreadProps) { // landed as the run's comment bubble, interstitial updates are ephemeral // (live-line only), and thinking stays in the run log / classic // transcript — "Worked · N tools" expands to exactly the tool rows. - const settledTurns: { turn: TaskChatTurnItem; anchorCommentId: string | null; startMs: number }[] = []; + const settledTurns: { + turn: TaskChatTurnItem; + anchorCommentId: string | null; + startMs: number; + }[] = []; // Raw summary inputs per turn id, so back-to-back same-agent runs can // coalesce into one "Worked" row in the final pass (PAP-362). const turnMergeMetaById = new Map(); @@ -642,9 +802,10 @@ export function TaskChatThread(props: TaskChatThreadProps) { const retryDetail = meta?.scheduledRetryAt ? "Retry scheduled automatically." : "You can retry this message now."; - const detail = code === "provider_frame_too_large" - ? `Provider output exceeded the safe limit. ${retryDetail}` - : `The runner stopped before returning an answer (${code}). ${retryDetail}`; + const detail = + code === "provider_frame_too_large" + ? `Provider output exceeded the safe limit. ${retryDetail}` + : `The runner stopped before returning an answer (${code}). ${retryDetail}`; const id = `${source.id}:failure`; entriesWithFailures.push({ ms: toMs(meta?.finishedAt ?? meta?.startedAt ?? meta?.createdAt), @@ -658,11 +819,30 @@ export function TaskChatThread(props: TaskChatThreadProps) { detail, }, }); + } else if (!lastCommentIdByRun.has(source.id)) { + settledRunIds.add(source.id); + const id = `${source.id}:terminal-notice`; + entriesWithFailures.push({ + ms: toMs(meta?.finishedAt ?? meta?.startedAt ?? meta?.createdAt), + order: 3, + id, + item: { + id, + kind: "marker", + variant: "turn_boundary", + label: "Run completed", + detail: "The runner returned no user-facing response.", + }, + }); } continue; } - const started = meta?.startedAt ? new Date(meta.startedAt).getTime() : NaN; - const finished = meta?.finishedAt ? new Date(meta.finishedAt).getTime() : NaN; + const started = meta?.startedAt + ? new Date(meta.startedAt).getTime() + : NaN; + const finished = meta?.finishedAt + ? new Date(meta.finishedAt).getTime() + : NaN; const durationMs = Number.isFinite(started) && Number.isFinite(finished) ? Math.max(0, finished - started) @@ -677,7 +857,27 @@ export function TaskChatThread(props: TaskChatThreadProps) { ? paperclipRunnerHistoryItems(parsed) : parsed, ); - if (children.length === 0 && source.adapterType !== "paperclip_runner") continue; + if (children.length === 0 && source.adapterType !== "paperclip_runner") + continue; + if ( + source.status === "succeeded" && + !lastCommentIdByRun.has(source.id) && + resolvedWithoutUserFacingResponse(meta?.resultJson) + ) { + const id = `${source.id}:terminal-notice`; + entriesWithFailures.push({ + ms: toMs(meta?.finishedAt ?? meta?.startedAt ?? meta?.createdAt), + order: 3, + id, + item: { + id, + kind: "marker", + variant: "turn_boundary", + label: "Run completed", + detail: "The runner returned no user-facing response.", + }, + }); + } settledRunIds.add(source.id); const failed = source.status !== "succeeded"; const startSlotRaw = meta?.startedAt ?? meta?.createdAt; @@ -704,7 +904,9 @@ export function TaskChatThread(props: TaskChatThreadProps) { startMs: startSlotRaw ? toMs(startSlotRaw) : Number.POSITIVE_INFINITY, }); } - settledTurns.sort((a, b) => (a.startMs < b.startMs ? -1 : a.startMs > b.startMs ? 1 : 0)); + settledTurns.sort((a, b) => + a.startMs < b.startMs ? -1 : a.startMs > b.startMs ? 1 : 0, + ); const turnsByAnchor = new Map(); const unanchored: { turn: TaskChatTurnItem; startMs: number }[] = []; @@ -725,7 +927,11 @@ export function TaskChatThread(props: TaskChatThreadProps) { entriesWithFailures.sort( (a, b) => a.ms - b.ms || a.order - b.order || a.id.localeCompare(b.id), ); - const out = assembleThreadItems(entriesWithFailures, turnsByAnchor, unanchored); + const out = assembleThreadItems( + entriesWithFailures, + turnsByAnchor, + unanchored, + ); // PAP-362: two runs replying back-to-back (same agent, nothing but the // agent's own bubbles between) fold into ONE "Worked" row below the last @@ -737,29 +943,45 @@ export function TaskChatThread(props: TaskChatThreadProps) { // every assembly/merge pass, so nothing can ever sort above it. return { items: prependIssueBrief( - attachSettledTurns(coalesceSettledTurns(out, turnMergeMetaById), turnMergeMetaById), + attachSettledTurns( + coalesceSettledTurns(out, turnMergeMetaById), + turnMergeMetaById, + ), hasBrief, ), settledRunIds, }; - }, [orderedEntries, runs, liveRun, transcriptByRun, linkedRunMetaById, lastCommentIdByRun, hasBrief]); + }, [ + orderedEntries, + runs, + liveRun, + transcriptByRun, + linkedRunMetaById, + lastCommentIdByRun, + hasBrief, + ]); // Hand off once the settled turn or its reply comment is in the thread; a // stopped run that yields neither is released by the backstop timeout so the // tail never lingers indefinitely. - const settlingIsPaperclipRunner = settlingRun != null && - runs.find((run) => run.id === settlingRun.id)?.adapterType === "paperclip_runner"; - const settlingHasComment = settlingRun != null && comments.some((comment) => - settlingIsPaperclipRunner - ? isRunnerResponseComment({ - comment, - runId: settlingRun.id, - runStartedAtMs: settlingRun.startedAtMs, - finalText: heldPaperclipRunnerFinalText, - }) - : comment.runId === settlingRun.id && !comment.deletedAt, - ); - const settledRunRendered = settlingRun != null && + const settlingIsPaperclipRunner = + settlingRun != null && + runs.find((run) => run.id === settlingRun.id)?.adapterType === + "paperclip_runner"; + const settlingHasComment = + settlingRun != null && + comments.some((comment) => + settlingIsPaperclipRunner + ? isRunnerResponseComment({ + comment, + runId: settlingRun.id, + runStartedAtMs: settlingRun.startedAtMs, + finalText: heldPaperclipRunnerFinalText, + }) + : comment.runId === settlingRun.id && !comment.deletedAt, + ); + const settledRunRendered = + settlingRun != null && (settlingIsPaperclipRunner ? settlingHasComment && settledRunIds.has(settlingRun.id) : settlingHasComment || settledRunIds.has(settlingRun.id)); @@ -769,7 +991,10 @@ export function TaskChatThread(props: TaskChatThreadProps) { setSettlingRun(null); return; } - const timer = window.setTimeout(() => setSettlingRun(null), SETTLING_TAIL_MAX_MS); + const timer = window.setTimeout( + () => setSettlingRun(null), + SETTLING_TAIL_MAX_MS, + ); return () => window.clearTimeout(timer); }, [settlingRun, settledRunRendered]); @@ -780,10 +1005,16 @@ export function TaskChatThread(props: TaskChatThreadProps) { settlingRun != null && !settledRunRendered && (transcriptByRun.get(settlingRun.id)?.length ?? 0) > 0; - const tailRunId = liveRun ? liveRun.id : showSettlingTail ? settlingRun!.id : null; + const tailRunId = liveRun + ? liveRun.id + : showSettlingTail + ? settlingRun!.id + : null; const tailStreaming = Boolean(liveRun); const tailEntries = tailRunId ? (transcriptByRun.get(tailRunId) ?? []) : []; - const tailAdapterType = tailRunId ? runs.find((run) => run.id === tailRunId)?.adapterType : null; + const tailAdapterType = tailRunId + ? runs.find((run) => run.id === tailRunId)?.adapterType + : null; const paperclipRunnerTail = tailAdapterType === "paperclip_runner"; const tailContentKey = tailEntries.reduce((total, entry) => { if ("text" in entry) return total + entry.text.length; @@ -803,13 +1034,16 @@ export function TaskChatThread(props: TaskChatThreadProps) { // while the pill's own second-tick keeps the elapsed readout moving. Through // the settle gap the run reads terminal, so the pill lands on its "Worked" // state instead of flipping back to a spinner. - const settlingFinishedAt = settlingRun ? linkedRunMetaById.get(settlingRun.id)?.finishedAt : undefined; + const settlingFinishedAt = settlingRun + ? linkedRunMetaById.get(settlingRun.id)?.finishedAt + : undefined; const tailStatus = liveRun ? liveRun.status - : runs.find((run) => run.id === settlingRun?.id)?.status ?? "succeeded"; + : (runs.find((run) => run.id === settlingRun?.id)?.status ?? "succeeded"); const tailStartedAtMs = liveRun - ? (liveRun.startedAt ? toMs(liveRun.startedAt) : null) ?? toMs(liveRun.createdAt) - : settlingRun?.startedAtMs ?? null; + ? ((liveRun.startedAt ? toMs(liveRun.startedAt) : null) ?? + toMs(liveRun.createdAt)) + : (settlingRun?.startedAtMs ?? null); const tailFinishedAtMs = liveRun ? liveRun.finishedAt ? toMs(liveRun.finishedAt) @@ -832,10 +1066,12 @@ export function TaskChatThread(props: TaskChatThreadProps) { // pill's own second-tick keeps the elapsed readout moving. `running` follows // tailStreaming: true while live, false through the settle gap — so the tail // renders identically either side of the run finishing. - const tailAgentName = liveRun?.agentName ?? linkedRunMetaById.get(tailRunId ?? "")?.agentName; - const tailAgentId = liveRun?.agentId - ?? linkedRunMetaById.get(tailRunId ?? "")?.agentId - ?? issueAssigneeAgentId; + const tailAgentName = + liveRun?.agentName ?? linkedRunMetaById.get(tailRunId ?? "")?.agentName; + const tailAgentId = + liveRun?.agentId ?? + linkedRunMetaById.get(tailRunId ?? "")?.agentId ?? + issueAssigneeAgentId; const tailAgent = tailAgentId ? agentMap?.get(tailAgentId) : undefined; const visibleTailAgentName = tailAgentName ?? tailAgent?.name ?? null; const visibleTailAgentIcon = tailAgent?.icon ?? null; @@ -852,47 +1088,66 @@ export function TaskChatThread(props: TaskChatThreadProps) { // eslint-disable-next-line react-hooks/exhaustive-deps [tailRunId, tailContentKey, tailStreaming, tailAgentName], ); - const pendingRuntimeRequest = tailItems.find( - (item): item is TaskChatRuntimeRequestItem => item.kind === "protocol" - && item.surface === "runtime_request" - && item.status === "pending", - ) ?? null; - const handleRuntimeRequestDecision = useCallback(async ( - item: TaskChatRuntimeRequestItem, - decision: TaskChatRuntimeRequestDecision, - ) => { - if (!paperclipRunnerTail || item.runId !== tailRunId || !item.turnId || !item.requestKind) { - throw new Error("This runtime request is no longer attached to the active Paperclip runner turn."); - } - let resolution: RuntimeRequestResolution; - if (decision.action !== "submit") { - resolution = decision; - } else if (item.requestKind === "user_input") { - resolution = { - action: "submit", - answers: Object.fromEntries( - Object.entries(decision.values).map(([field, value]) => [field, { answers: [value] }]), - ), - }; - } else if (item.requestKind === "elicitation") { - resolution = { action: "submit", content: decision.values }; - } else { - throw new Error("This runtime permission does not accept submitted form data."); - } - await heartbeatsApi.resolveRuntimeRequest({ - runId: item.runId, - requestId: item.requestId, - turnId: item.turnId, - requestKind: item.requestKind, - resolution, - }); - }, [paperclipRunnerTail, tailRunId]); - const runtimeComposerDisabledReason = composerDisabledReason - ?? (paperclipRunnerTail && pendingRuntimeRequest + const pendingRuntimeRequest = + tailItems.find( + (item): item is TaskChatRuntimeRequestItem => + item.kind === "protocol" && + item.surface === "runtime_request" && + item.status === "pending", + ) ?? null; + const handleRuntimeRequestDecision = useCallback( + async ( + item: TaskChatRuntimeRequestItem, + decision: TaskChatRuntimeRequestDecision, + ) => { + if ( + !paperclipRunnerTail || + item.runId !== tailRunId || + !item.turnId || + !item.requestKind + ) { + throw new Error( + "This runtime request is no longer attached to the active Paperclip runner turn.", + ); + } + let resolution: RuntimeRequestResolution; + if (decision.action !== "submit") { + resolution = decision; + } else if (item.requestKind === "user_input") { + resolution = { + action: "submit", + answers: Object.fromEntries( + Object.entries(decision.values).map(([field, value]) => [ + field, + { answers: [value] }, + ]), + ), + }; + } else if (item.requestKind === "elicitation") { + resolution = { action: "submit", content: decision.values }; + } else { + throw new Error( + "This runtime permission does not accept submitted form data.", + ); + } + await heartbeatsApi.resolveRuntimeRequest({ + runId: item.runId, + requestId: item.requestId, + turnId: item.turnId, + requestKind: item.requestKind, + resolution, + }); + }, + [paperclipRunnerTail, tailRunId], + ); + const runtimeComposerDisabledReason = + composerDisabledReason ?? + (paperclipRunnerTail && pendingRuntimeRequest ? "Resolve the runtime request before sending another message." : undefined); const assigneeUsesPaperclipRunner = Boolean( - issueAssigneeAgentId && agentMap?.get(issueAssigneeAgentId)?.adapterType === "paperclip_runner", + issueAssigneeAgentId && + agentMap?.get(issueAssigneeAgentId)?.adapterType === "paperclip_runner", ); const [runnerSubmissionPending, setRunnerSubmissionPending] = useState(false); useEffect(() => { @@ -900,7 +1155,10 @@ export function TaskChatThread(props: TaskChatThreadProps) { }, [tailRunId]); useEffect(() => { if (!runnerSubmissionPending || tailRunId) return; - const timeout = window.setTimeout(() => setRunnerSubmissionPending(false), 10_000); + const timeout = window.setTimeout( + () => setRunnerSubmissionPending(false), + 10_000, + ); return () => window.clearTimeout(timeout); }, [runnerSubmissionPending, tailRunId]); const handleThreadAdd = useCallback( @@ -915,11 +1173,16 @@ export function TaskChatThread(props: TaskChatThreadProps) { }, [assigneeUsesPaperclipRunner, onAdd], ); - const optimisticRunnerStartup = assigneeUsesPaperclipRunner && !tailRunId && ( - runnerSubmissionPending || commentItems.some( - (item) => item.kind === "message" && item.author === "human" && item.optimistic === "pending", - ) - ); + const optimisticRunnerStartup = + assigneeUsesPaperclipRunner && + !tailRunId && + (runnerSubmissionPending || + commentItems.some( + (item) => + item.kind === "message" && + item.author === "human" && + item.optimistic === "pending", + )); // Feedback votes keyed by the comment they target (targetType // "issue_comment"), mirroring IssueChatThread — the redesign attaches the @@ -955,14 +1218,20 @@ export function TaskChatThread(props: TaskChatThreadProps) { /> ); }, - [onVote, feedbackVoteByTargetId, feedbackDataSharingPreference, feedbackTermsUrl], + [ + onVote, + feedbackVoteByTargetId, + feedbackDataSharingPreference, + feedbackTermsUrl, + ], ); const renderQueuedAction = useCallback( (item: TaskChatMessageItem) => { if (paperclipQueue) return null; const runId = item.queueTargetRunId; - if (item.optimistic !== "queued" || !runId || !onInterruptQueued) return null; + if (item.optimistic !== "queued" || !runId || !onInterruptQueued) + return null; const isInterrupting = interruptingQueuedRunId === runId; return ( @@ -1022,12 +1291,17 @@ export function TaskChatThread(props: TaskChatThreadProps) { return (
{items.length === 0 && !tailRunId ? ( -
+
{threadHeaderWithBlockers ? (
) : null} -
{emptyMessage}
+
+ {emptyMessage} +
{bottomBlockerLinks ? (
{bottomBlockerLinks} @@ -1049,48 +1325,57 @@ export function TaskChatThread(props: TaskChatThreadProps) { header={threadHeaderWithBlockers} onRuntimeRequestDecision={handleRuntimeRequestDecision} renderInteraction={renderInteraction} - renderBrief={issueBrief ? () => : undefined} + renderBrief={ + issueBrief + ? () => + : undefined + } renderMessageActions={renderMessageActions} renderQueuedAction={renderQueuedAction} - tail={tailRunId || optimisticRunnerStartup || bottomBlockerLinks ? ( - <> - {tailRunId || optimisticRunnerStartup ? ( -
- {paperclipRunnerTail || optimisticRunnerStartup ? ( - - ) : ( - <> - + {tailRunId || optimisticRunnerStartup ? ( +
+ {paperclipRunnerTail || optimisticRunnerStartup ? ( + - - - )} -
- ) : null} - {bottomBlockerLinks} - - ) : null} + ) : ( + <> + + + + )} +
+ ) : null} + {bottomBlockerLinks} + + ) : null + } contentKey={threadContentKey} scroll={!isMobile} /> diff --git a/ui/src/components/task-chat/TaskChatLiveRunPill.test.tsx b/ui/src/components/task-chat/TaskChatLiveRunPill.test.tsx index ce1c8f26de4..4f43289f8d6 100644 --- a/ui/src/components/task-chat/TaskChatLiveRunPill.test.tsx +++ b/ui/src/components/task-chat/TaskChatLiveRunPill.test.tsx @@ -90,43 +90,4 @@ describe("TaskChatLiveRunPill", () => { expect(pill?.textContent).toContain("ran 1 command"); }); - it("keeps the paperclip runner Working header static", () => { - act(() => { - root.render( - , - ); - }); - - expect(container.textContent).toContain("Workingfor 2s"); - expect(container.querySelector(".shimmer-text")).toBeNull(); - }); - - it("only exposes the runner disclosure caret after the run settles", () => { - const render = (status: string) => act(() => { - root.render( - undefined} - />, - ); - }); - - render("running"); - expect(container.querySelector('[data-testid="task-chat-live-run-pill"]')?.tagName).toBe("DIV"); - expect(container.querySelector('[data-testid="task-chat-live-run-pill"] svg')).toBeNull(); - - render("succeeded"); - expect(container.querySelector('[data-testid="task-chat-live-run-pill"]')?.tagName).toBe("BUTTON"); - expect(container.querySelector('[data-testid="task-chat-live-run-pill"] svg')).not.toBeNull(); - }); }); diff --git a/ui/src/components/task-chat/TaskChatLiveRunPill.tsx b/ui/src/components/task-chat/TaskChatLiveRunPill.tsx index 2d236582797..cf033f08bf3 100644 --- a/ui/src/components/task-chat/TaskChatLiveRunPill.tsx +++ b/ui/src/components/task-chat/TaskChatLiveRunPill.tsx @@ -1,4 +1,4 @@ -import { ChevronRight, Loader2 } from "lucide-react"; +import { Loader2 } from "lucide-react"; import type { TranscriptEntry } from "../../adapters"; import { cn } from "@/lib/utils"; import { useSecondTick } from "@/hooks/useSecondTick"; @@ -47,9 +47,6 @@ export function TaskChatLiveRunPill({ startedAtMs, finishedAtMs, toolSummary, - runnerStyle = false, - expanded, - onToggle, }: { status: string; /** Run start (startedAt, falling back to createdAt) in ms, or null if unknown. */ @@ -57,10 +54,6 @@ export function TaskChatLiveRunPill({ /** Run finish in ms once terminal; drives the settled elapsed readout. */ finishedAtMs?: number | null; toolSummary: string | null; - /** Codex-style new-runner header: no spinner, whole-second compact duration. */ - runnerStyle?: boolean; - expanded?: boolean; - onToggle?: () => void; }) { const active = !isTerminalRunStatus(status); // One shared page-wide ticker drives the live elapsed readout, matching the @@ -70,43 +63,11 @@ export function TaskChatLiveRunPill({ const elapsedMs = startedAtMs == null ? null : (active ? Date.now() : finishedAtMs ?? Date.now()) - startedAtMs; const elapsed = elapsedMs != null - ? runnerStyle - ? `${Math.max(0, Math.floor(elapsedMs / 1000))}s` - : formatDurationWords(elapsedMs) + ? formatDurationWords(elapsedMs) : null; const verb = active ? "Working" : "Worked"; const suffix = elapsed ? `for ${elapsed}` : null; - if (runnerStyle) { - const content = ( - <> - {verb} - {suffix ? {suffix} : null} - {!active ? ( - - ) : null} - - ); - return onToggle && !active ? ( - - ) : ( -
- {content} -
- ); - } - return (
{ + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + }); + + function render(item: TaskChatProtocolItem) { + act(() => root.render( + + + + + , + )); + } + + it("renders research as one compact line with expandable sources", () => { + const item: TaskChatProviderActivityItem = { + id: "research", + kind: "protocol", + surface: "provider_activity", + family: "research", + eventType: "research.completed", + status: "completed", + title: "Research", + summary: "smoked pork shoulder", + details: [{ label: "Query", value: "smoked pork shoulder", mono: true }], + steps: [], + links: [{ label: "Smoking guide", href: "https://example.com/smoking", description: "Temperature targets" }], + children: [], + }; + render(item); + + const row = container.querySelector('[data-testid="task-chat-protocol-activity-row"] button'); + expect(container.querySelector("article")).toBeNull(); + expect(row?.textContent).toContain("Searched the web"); + expect(row?.textContent).toContain("smoked pork shoulder"); + expect(row?.getAttribute("aria-expanded")).toBe("false"); + expect(row?.getAttribute("aria-controls")).toMatch(/^task-chat-protocol-activity-/); + + act(() => row?.click()); + + expect(row?.getAttribute("aria-expanded")).toBe("true"); + const detail = container.querySelector('[data-testid="task-chat-protocol-activity-detail"]'); + expect(detail?.id).toBe(row?.getAttribute("aria-controls")); + expect(detail?.textContent).toContain("Smoking guide"); + }); + + it("keeps workspace files and diffs inside the row detail", () => { + render({ + id: "workspace", + kind: "protocol", + surface: "workspace_change", + changeSetId: "change-1", + revision: 1, + source: "runner_verified", + complete: true, + files: [{ path: "ui/src/App.tsx", operation: "modify", previousPath: null, additions: 1, deletions: 1, binary: false, diff: "-old\n+new" }], + totals: { files: 1, additions: 1, deletions: 1 }, + patchArtifactRef: null, + }); + + const row = container.querySelector('[data-testid="task-chat-protocol-activity-row"] button'); + expect(row?.textContent).toContain("Edited files"); + act(() => row?.click()); + expect(container.textContent).toContain("ui/src/App.tsx"); + expect(container.textContent).toContain("+new"); + }); + + it("renders durable resources as direct compact links", () => { + render({ + id: "resource", + kind: "protocol", + surface: "resource", + resourceKind: "document", + title: "Implementation notes", + subtitle: "markdown · ready", + href: "/documents/notes", + }); + + const link = container.querySelector('[data-testid="task-chat-protocol-activity-row"]'); + expect(link?.getAttribute("href")).toBe("/documents/notes"); + expect(link?.textContent).toContain("Added a document"); + expect(link?.textContent).toContain("Implementation notes"); + }); + + it("does not make an informational row focusable when it has no details", () => { + render({ + id: "notice", + kind: "protocol", + surface: "provider_activity", + family: "provider_notice", + eventType: "provider.notice.recorded", + status: "informational", + title: "Provider notice", + summary: "Retrying automatically", + details: [], + steps: [], + links: [], + children: [], + }); + + const row = container.querySelector('[data-testid="task-chat-protocol-activity-row"]'); + expect(row?.querySelector("button")).toBeNull(); + expect(row?.querySelector('[aria-expanded]')).toBeNull(); + }); +}); diff --git a/ui/src/components/task-chat/TaskChatProtocolActivityRow.tsx b/ui/src/components/task-chat/TaskChatProtocolActivityRow.tsx new file mode 100644 index 00000000000..516c97f47b1 --- /dev/null +++ b/ui/src/components/task-chat/TaskChatProtocolActivityRow.tsx @@ -0,0 +1,228 @@ +import { useId, useState, type ReactNode } from "react"; +import { + Check, + ChevronRight, + Circle, + ExternalLink, + Loader2, + X, +} from "lucide-react"; +import { MarkdownBody } from "@/components/MarkdownBody"; +import { WorkspaceFileLink } from "@/components/WorkspaceFileLink"; +import { cn } from "@/lib/utils"; +import type { + TaskChatProtocolItem, + TaskChatProtocolStep, + TaskChatProviderActivityItem, + TaskChatWorkspaceChangeItem, + TaskChatWorkspaceFileItem, +} from "./task-chat-model"; +import { + protocolActivityIsRunning, + protocolActivityLabel, + protocolActivityPresentation, +} from "./task-chat-activity-presentation"; + +function StatusIcon({ status }: { status: "running" | "completed" | "failed" | "interrupted" | "informational" }) { + if (status === "running") return ; + if (status === "failed" || status === "interrupted") return ; + if (status === "completed") return ; + return ; +} + +function stepStatusIcon(status: TaskChatProtocolStep["status"]) { + if (status === "in_progress") return ; + if (status === "completed") return ; + if (status === "blocked" || status === "failed") return ; + return ; +} + +function ProviderDetails({ item }: { item: TaskChatProviderActivityItem }) { + return ( +
+ {item.steps.length > 0 ? ( +
    + {item.steps.map((step) => ( +
  1. + {stepStatusIcon(step.status)} + {step.label} +
  2. + ))} +
+ ) : null} + {item.links.length > 0 ? ( +
    + {item.links.map((link) => ( +
  • + + {link.label} + + + {link.description ?

    {link.description}

    : null} +
  • + ))} +
+ ) : null} + {item.children.length > 0 ? ( +
    + {item.children.map((child) => ( +
  • + + {child.title} + {child.status} + {child.metadata ? {child.metadata} : null} + + {child.summary ? {child.summary} : null} +
  • + ))} +
+ ) : null} + {item.details.length > 0 ? ( +
+ {item.details.map((detail) => ( +
+
{detail.label}
+
{detail.value}
+
+ ))} +
+ ) : null} + {item.output ? ( +
{item.output}
+ ) : null} + {item.outputTruncated ?

Output truncated to 8 KiB.

: null} +
+ ); +} + +function WorkspaceChangeDetails({ item }: { item: TaskChatWorkspaceChangeItem }) { + return item.files.length > 0 ? ( +
    + {item.files.map((file) => ( +
  • + + + {file.previousPath ? `${file.previousPath} → ${file.path}` : file.path} + + {file.operation.replaceAll("_", " ")} + {file.additions != null || file.deletions != null ? ( + + {file.additions == null ? "" : `+${file.additions}`} {file.deletions == null ? "" : `−${file.deletions}`} + + ) : null} + + {file.diff ?
    {file.diff}
    : null} +
  • + ))} +
+ ) :

No changed-file details were reported.

; +} + +function WorkspaceFileDetails({ item }: { item: TaskChatWorkspaceFileItem }) { + const workspaceFileRef = { + path: item.path, + resourceKind: "file" as const, + line: item.line, + column: null, + raw: item.path, + }; + return ( +
+ + {item.preview ? ( + item.presentation === "document" ? ( +
{item.preview}
+ ) : ( +
{item.preview}
+ ) + ) :

Preview unavailable.

} + {item.previewTruncated ?

Preview truncated by the runner.

: null} +
+ ); +} + +function detailContent(item: TaskChatProtocolItem): ReactNode | null { + switch (item.surface) { + case "provider_activity": { + const expandable = item.details.length > 0 || item.steps.length > 0 || item.links.length > 0 || item.children.length > 0 || Boolean(item.output) || Boolean(item.outputTruncated); + return expandable ? : null; + } + case "workspace_change": return ; + case "workspace_file": return ; + case "resource": + case "runtime_request": + case "run_result": + case "run_terminal": + return null; + } +} + +function itemStatus(item: TaskChatProtocolItem): "running" | "completed" | "failed" | "interrupted" | "informational" { + if (item.surface === "provider_activity") return item.status; + if (item.surface === "workspace_change") return item.complete ? "completed" : "running"; + return "completed"; +} + +export function TaskChatProtocolActivityRow({ item }: { item: TaskChatProtocolItem }) { + const [open, setOpen] = useState(false); + const detailId = `task-chat-protocol-activity-${useId().replaceAll(":", "")}`; + const presentation = protocolActivityPresentation(item); + if (!presentation) return null; + const detail = detailContent(item); + const expandable = detail !== null; + const Icon = presentation.icon; + const label = protocolActivityLabel(item, presentation); + const active = protocolActivityIsRunning(item); + const status = itemStatus(item); + const row = ( + <> + + {label}{active ? "…" : ""} + {presentation.detail ? {presentation.detail} : null} + + {expandable ? ( + + ) : null} + + + + ); + + if (item.surface === "resource" && item.href) { + return ( + + {row} + + ); + } + + const activityRow = expandable ? ( + + ) : ( +
+ {row} +
+ ); + + return ( +
+ {activityRow} + {open && detail ? ( +
+ {detail} +
+ ) : null} +
+ ); +} diff --git a/ui/src/components/task-chat/TaskChatProtocolCard.test.tsx b/ui/src/components/task-chat/TaskChatProtocolCard.test.tsx index 36d6ea61834..aa5ffc01d67 100644 --- a/ui/src/components/task-chat/TaskChatProtocolCard.test.tsx +++ b/ui/src/components/task-chat/TaskChatProtocolCard.test.tsx @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ThemeProvider } from "@/context/ThemeContext"; import { MemoryRouter } from "@/lib/router"; import { TaskChatProtocolCard } from "./TaskChatProtocolCard"; -import type { TaskChatProtocolItem, TaskChatRuntimeRequestDecision } from "./task-chat-model"; +import type { TaskChatProtocolItem, TaskChatProviderActivityFamily, TaskChatRuntimeRequestDecision } from "./task-chat-model"; function renderCard(root: Root, item: TaskChatProtocolItem, onDecision?: (decision: TaskChatRuntimeRequestDecision) => void | Promise) { flushSync(() => root.render( @@ -54,6 +54,31 @@ describe("TaskChatProtocolCard", () => { expect(container.textContent).toContain("Render widgets"); }); + it("renders a structured history card for every provider family", () => { + const families = [ + "plan", "tool_execution", "research", "delegation", "model_identity", "context", "artifact", + "review", "hook", "memory", "safety", "terminal", "wait", "provider_notice", + ] satisfies TaskChatProviderActivityFamily[]; + for (const family of families) { + renderCard(root, { + id: `provider-${family}`, + kind: "protocol", + surface: "provider_activity", + family, + eventType: `${family}.fixture`, + status: "completed", + title: `Visible ${family}`, + summary: `Summary ${family}`, + details: [{ label: "Reference", value: `${family}-1` }], + links: [], + children: [], + steps: [], + }); + expect(container.querySelector(`[data-testid="task-chat-provider-${family}"]`), family).not.toBeNull(); + expect(container.textContent, family).toContain(`Visible ${family}`); + } + }); + it("opens a workspace diff review dialog", async () => { renderCard(root, { id: "workspace", diff --git a/ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx b/ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx index bdb1f2e6ae6..8d428f4871a 100644 --- a/ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx +++ b/ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx @@ -4,8 +4,15 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ThemeProvider } from "@/context/ThemeContext"; +import { MemoryRouter } from "@/lib/router"; import { TaskChatRunnerTurn } from "./TaskChatRunnerTurn"; -import type { TaskChatItem, TaskChatRuntimeRequestDecision, TaskChatRuntimeRequestItem } from "./task-chat-model"; +import type { + TaskChatItem, + TaskChatProviderActivityFamily, + TaskChatProviderActivityItem, + TaskChatRuntimeRequestDecision, + TaskChatRuntimeRequestItem, +} from "./task-chat-model"; describe("TaskChatRunnerTurn", () => { let container: HTMLDivElement; @@ -28,16 +35,21 @@ describe("TaskChatRunnerTurn", () => { runId = "run-1", onRuntimeRequestDecision?: (item: TaskChatRuntimeRequestItem, decision: TaskChatRuntimeRequestDecision) => void, ) => act(() => root.render( - - - , + + + + + , )); it("shows immediate Thinking before the first runner event", () => { render([], "queued"); expect(container.textContent).toContain("Thinking"); expect(container.textContent).toContain("Runner"); - expect(container.querySelector('[data-testid="task-chat-current-activity"] svg')).toBeNull(); + expect(container.querySelector('[data-testid="task-chat-runner-disclosure-caret"]')).toBeNull(); + expect(container.querySelector('[data-testid="task-chat-current-activity-icon"]')).toBeNull(); + expect(container.querySelector('[data-testid="task-chat-current-activity-label"]')?.textContent).toBe("Thinking"); + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.tagName).toBe("DIV"); expect(container.textContent).not.toContain("Working"); expect(container.textContent).not.toContain("Waiting for transcript"); const identity = container.querySelector('[data-testid="task-chat-agent-identity"]'); @@ -59,14 +71,141 @@ describe("TaskChatRunnerTurn", () => { expect(container.querySelector('[data-testid="task-chat-runner-identity-row"]')?.classList.contains("pt-2")).toBe(true); }); - it("streams the final response in its durable slot and hides current activity", () => { + it("shows canonical web research as the current Codex-style activity", () => { + render([{ + id: "research-1", + kind: "protocol", + surface: "provider_activity", + family: "research", + eventType: "research.started", + status: "running", + title: "Research", + summary: "site:openai.com model guide GPT-5.4", + details: [{ label: "Query", value: "site:openai.com model guide GPT-5.4" }], + steps: [], + links: [], + children: [], + }]); + + const activity = container.querySelector('[data-testid="task-chat-current-activity"]'); + expect(activity?.textContent).toContain("Searching the web"); + expect(activity?.textContent).toContain("site:openai.com model guide GPT-5.4"); + expect(activity?.getAttribute("data-activity-family")).toBe("research"); + }); + + it("has a purpose-built current-activity presentation for every provider family", () => { + const cases: Array<{ + family: TaskChatProviderActivityFamily; + eventType: string; + status: TaskChatProviderActivityItem["status"]; + expected: string; + details?: TaskChatProviderActivityItem["details"]; + }> = [ + { family: "plan", eventType: "plan.updated", status: "running", expected: "Updating the plan" }, + { family: "tool_execution", eventType: "tool.execution.started", status: "running", expected: "Running a tool" }, + { family: "research", eventType: "research.started", status: "running", expected: "Searching the web", details: [{ label: "Action", value: "search" }] }, + { family: "delegation", eventType: "delegation.started", status: "running", expected: "Starting a subagent", details: [{ label: "Action", value: "spawn" }] }, + { family: "model_identity", eventType: "model.route.changed", status: "informational", expected: "Switched models" }, + { family: "context", eventType: "context.compacted", status: "completed", expected: "Compacted context" }, + { family: "artifact", eventType: "artifact.generated", status: "running", expected: "Generating an artifact" }, + { family: "review", eventType: "review.mode.changed", status: "informational", expected: "Entered review mode", details: [{ label: "State", value: "entered" }] }, + { family: "hook", eventType: "hook.started", status: "running", expected: "Running a hook" }, + { family: "memory", eventType: "memory.citation.referenced", status: "informational", expected: "Referenced memory" }, + { family: "safety", eventType: "safety.review.started", status: "running", expected: "Reviewing safety" }, + { family: "terminal", eventType: "terminal.input.sent", status: "informational", expected: "Sent terminal input" }, + { family: "wait", eventType: "wait.started", status: "running", expected: "Waiting" }, + { family: "provider_notice", eventType: "provider.notice.recorded", status: "informational", expected: "Provider notice" }, + ]; + + expect(cases.map((entry) => entry.family)).toEqual([ + "plan", "tool_execution", "research", "delegation", "model_identity", "context", "artifact", + "review", "hook", "memory", "safety", "terminal", "wait", "provider_notice", + ] satisfies TaskChatProviderActivityFamily[]); + + for (const entry of cases) { + render([{ + id: `provider-${entry.family}`, + kind: "protocol", + surface: "provider_activity", + family: entry.family, + eventType: entry.eventType, + status: entry.status, + title: entry.family, + details: entry.details ?? [], + steps: [], + links: [], + children: [], + }]); + const activity = container.querySelector('[data-testid="task-chat-current-activity"]'); + expect(activity?.getAttribute("data-activity-family"), entry.family).toBe(entry.family); + expect(activity?.textContent, entry.family).toContain(entry.expected); + } + }); + + it("shows provider failures and interruptions instead of a successful past-tense label", () => { + const provider = (status: "failed" | "interrupted"): TaskChatProviderActivityItem => ({ + id: `research-${status}`, + kind: "protocol", + surface: "provider_activity", + family: "research", + eventType: "research.completed", + status, + title: "Research", + details: [], + steps: [], + links: [], + children: [], + }); + render([provider("failed")]); + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.textContent).toContain("Web search failed"); + render([provider("interrupted")]); + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.textContent).toContain("Web search stopped"); + }); + + it("surfaces workspace changes and file references as current activity", () => { + render([{ + id: "workspace-change", + kind: "protocol", + surface: "workspace_change", + changeSetId: "change-1", + revision: 1, + source: "harness_reported", + complete: false, + files: [], + totals: { files: 2, additions: 3, deletions: 1 }, + patchArtifactRef: null, + }]); + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.textContent).toContain("Editing files"); + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.textContent).toContain("2 files"); + + render([{ + id: "workspace-file", + kind: "protocol", + surface: "workspace_file", + referenceId: "file-1", + source: "runner_verified", + path: "ui/src/App.tsx", + displayName: "App.tsx", + mediaType: "text/typescript", + presentation: "code", + line: 42, + preview: null, + previewTruncated: false, + }]); + const activity = container.querySelector('[data-testid="task-chat-current-activity"]'); + expect(activity?.textContent).toContain("Referenced a file"); + expect(activity?.textContent).toContain("ui/src/App.tsx:42"); + }); + + it("streams the final response in its durable slot and settles the disclosure", () => { render([ { id: "p1", kind: "message", author: "agent", text: "Checking.", interstitial: true, channel: "progress" }, { id: "f1", kind: "message", author: "agent", authorName: "Runner", text: "Completed successfully.", channel: "final", streaming: true }, - ]); + ], "succeeded"); expect(container.querySelector('[data-testid="task-chat-final-response"]')?.textContent).toContain("Completed successfully."); expect(container.querySelectorAll('[data-testid="task-chat-agent-avatar"]')).toHaveLength(1); - expect(container.querySelector('[data-testid="task-chat-current-activity"]')).toBeNull(); + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.textContent).toContain("Worked for"); + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.getAttribute("aria-expanded")).toBe("false"); }); it("keeps final text mounted through a transient replay gap", () => { @@ -101,7 +240,7 @@ describe("TaskChatRunnerTurn", () => { { id: "complete", kind: "marker", variant: "turn_boundary", label: "Turn completed" }, ]); - expect(container.textContent).toContain("Paperclip_finish"); + expect(container.textContent).not.toContain("Paperclip_finish"); expect(container.textContent).toContain("Interrupted"); expect(container.textContent).not.toContain("Session started"); expect(container.textContent).not.toContain("Turn started"); @@ -133,4 +272,62 @@ describe("TaskChatRunnerTurn", () => { await act(async () => button?.click()); expect(onDecision).toHaveBeenCalledWith(expect.objectContaining({ requestId: "request-1" }), { action: "accept" }); }); + + it("expands live semantic activity from the current activity row", () => { + render([ + { id: "thinking-empty", kind: "thinking", lines: [], streaming: false, lifecycleOnly: true }, + { id: "commentary", kind: "message", author: "agent", text: "Checking the current implementation.", interstitial: true, channel: "progress" }, + { id: "tool", kind: "tool", name: "Read", rawName: "read_file", target: "ui/src/App.tsx", status: "completed", detail: "export function App() {}" }, + ]); + + const disclosure = container.querySelector('[data-testid="task-chat-current-activity"]'); + expect(disclosure?.getAttribute("aria-expanded")).toBe("false"); + expect(container.querySelector('[data-testid="task-chat-runner-disclosure-caret"]')).not.toBeNull(); + expect(container.querySelectorAll('[data-testid="task-chat-thinking"]')).toHaveLength(0); + + act(() => disclosure?.click()); + + expect(disclosure?.getAttribute("aria-expanded")).toBe("true"); + expect(container.querySelector('[data-testid="task-chat-runner-activity-list"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="task-chat-progress-update"]')).toBeNull(); + expect(container.querySelector('[data-testid="task-chat-activity-commentary"]')?.textContent).toContain("Checking the current implementation."); + expect(container.textContent).toContain("ui/src/App.tsx"); + }); + + it("collapses an open activity list when the run becomes terminal", () => { + const items: TaskChatItem[] = [{ id: "tool", kind: "tool", name: "Search", rawName: "web_search", target: "smoked pork shoulder", status: "in_progress" }]; + render(items); + const disclosure = container.querySelector('[data-testid="task-chat-current-activity"]'); + act(() => disclosure?.click()); + expect(disclosure?.getAttribute("aria-expanded")).toBe("true"); + + render([{ ...items[0], kind: "tool", status: "completed" } as TaskChatItem], "succeeded"); + + const settled = container.querySelector('[data-testid="task-chat-current-activity"]'); + expect(settled?.getAttribute("aria-expanded")).toBe("false"); + expect(settled?.textContent).toContain("Worked for"); + expect(container.querySelector(".tc-turn-fold")?.getAttribute("data-folded")).toBe("true"); + }); + + it("resets the disclosure choice when a different run takes over", () => { + const items: TaskChatItem[] = [{ id: "tool", kind: "tool", name: "Read", rawName: "read_file", target: "ui/src/App.tsx", status: "completed" }]; + render(items, "running", "run-1"); + act(() => container.querySelector('[data-testid="task-chat-current-activity"]')?.click()); + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.getAttribute("aria-expanded")).toBe("true"); + + render(items, "running", "run-2"); + + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.getAttribute("aria-expanded")).toBe("false"); + expect(container.querySelector(".tc-turn-fold")?.getAttribute("data-folded")).toBe("true"); + }); + + it("renders a failed terminal disclosure without treating prose as status authority", () => { + render([ + { id: "final", kind: "message", author: "agent", text: "I finished the draft.", channel: "final" }, + { id: "tool", kind: "tool", name: "Command", rawName: "bash", target: "pnpm test", status: "failed" }, + ], "failed"); + + expect(container.querySelector('[data-testid="task-chat-current-activity"]')?.textContent).toContain("Stopped after"); + expect(container.querySelector('[data-testid="task-chat-final-response"]')?.textContent).toContain("I finished the draft."); + }); }); diff --git a/ui/src/components/task-chat/TaskChatRunnerTurn.tsx b/ui/src/components/task-chat/TaskChatRunnerTurn.tsx index 40f88ac3d23..6f4ef21b016 100644 --- a/ui/src/components/task-chat/TaskChatRunnerTurn.tsx +++ b/ui/src/components/task-chat/TaskChatRunnerTurn.tsx @@ -1,12 +1,32 @@ -import { useRef, useState } from "react"; +import { useId, useRef, useState, type ComponentType, type SVGProps } from "react"; +import { Check, ChevronRight, OctagonX, X } from "lucide-react"; import { MarkdownBody } from "@/components/MarkdownBody"; +import { useSecondTick } from "@/hooks/useSecondTick"; import { cn } from "@/lib/utils"; -import type { TaskChatItem, TaskChatMessageItem, TaskChatRuntimeRequestDecision, TaskChatRuntimeRequestItem, TaskChatThinkingItem, TaskChatToolItem } from "./task-chat-model"; +import type { + TaskChatItem, + TaskChatMessageItem, + TaskChatMarkerItem, + TaskChatProtocolItem, + TaskChatRuntimeRequestDecision, + TaskChatRuntimeRequestItem, + TaskChatThinkingItem, + TaskChatToolItem, +} from "./task-chat-model"; import { TaskChatAgentIdentity } from "./TaskChatBubble"; -import { TaskChatLiveRunPill } from "./TaskChatLiveRunPill"; -import { TaskChatLiveTail } from "./TaskChatLiveTail"; +import { TaskChatProtocolActivityRow } from "./TaskChatProtocolActivityRow"; import { TaskChatProtocolCard } from "./TaskChatProtocolCard"; -import { paperclipRunnerHistoryItems } from "./transcript-adapter"; +import { TaskChatThinking } from "./TaskChatThinking"; +import { TaskChatToolCard } from "./TaskChatToolCard"; +import { + protocolActivityIsRunning, + protocolActivityLabel, + protocolActivityPresentation, +} from "./task-chat-activity-presentation"; +import { + isTerminalRunStatus, + paperclipRunnerActivityItems, +} from "./transcript-adapter"; import { toolTaxonomy } from "./tool-taxonomy"; function lastOf(items: readonly TaskChatItem[], predicate: (item: TaskChatItem) => item is T): T | undefined { @@ -17,26 +37,173 @@ function lastOf(items: readonly TaskChatItem[], predicat return undefined; } -function CurrentActivity({ items }: { items: readonly TaskChatItem[] }) { - const activity = lastOf( - items, - (item): item is TaskChatThinkingItem | TaskChatToolItem => item.kind === "thinking" || item.kind === "tool", - ); - if (!activity || activity.kind === "thinking") { - return ( -
- Thinking -
- ); - } - const taxonomy = toolTaxonomy(activity.rawName ?? activity.name); - const Icon = taxonomy.icon; - const active = activity.status === "pending" || activity.status === "in_progress"; +function isHeadlineProtocolActivity(item: TaskChatItem): item is TaskChatProtocolItem { + return item.kind === "protocol" && protocolActivityPresentation(item) !== null; +} + +function formatCompactDuration(ms: number | null): string | null { + if (ms == null || !Number.isFinite(ms)) return null; + const totalSeconds = Math.max(0, Math.floor(ms / 1000)); + if (totalSeconds < 60) return `${totalSeconds}s`; + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`; +} + +function terminalStatusFailed(status: string): boolean { + return status === "failed" + || status === "cancelled" + || status === "timed_out" + || status === "interrupted"; +} + +function RunnerActivityTimeline({ items }: { items: readonly TaskChatItem[] }) { + if (items.length === 0) return null; return ( -
- - {taxonomy.verbLabel} - {activity.target ? {activity.target} : null} +
+ +
    + {items.map((item) => ( +
  1. + {item.kind === "message" ? ( +
    + {item.text} +
    + ) : item.kind === "thinking" ? ( + + ) : item.kind === "tool" ? ( + + ) : item.kind === "marker" ? ( + + ) : item.kind === "protocol" ? ( + + ) : null} +
  2. + ))} +
+
+ ); +} + +function RunnerActivityMarker({ item }: { item: TaskChatMarkerItem }) { + return ( +
+ + {item.label} + {item.detail ? {item.detail} : null} +
+ ); +} + +function RunnerActivityDisclosure({ + items, + status, + startedAtMs, + finishedAtMs, + open, + onToggle, + controlsId, +}: { + items: readonly TaskChatItem[]; + status: string; + startedAtMs: number | null; + finishedAtMs?: number | null; + open: boolean; + onToggle: () => void; + controlsId: string; +}) { + const terminal = isTerminalRunStatus(status); + useSecondTick(!terminal && startedAtMs != null); + const elapsedMs = startedAtMs == null + ? null + : Math.max(0, (terminal ? finishedAtMs ?? Date.now() : Date.now()) - startedAtMs); + const elapsed = formatCompactDuration(elapsedMs); + const activity = lastOf( + items, + (item): item is TaskChatThinkingItem | TaskChatToolItem | TaskChatProtocolItem => + item.kind === "thinking" || item.kind === "tool" || isHeadlineProtocolActivity(item), + ); + + let Icon: ComponentType> | null = null; + let label = "Thinking"; + let detail: string | undefined; + let active = !terminal; + let family: string | undefined; + if (terminal) { + const failed = terminalStatusFailed(status); + Icon = failed ? X : Check; + label = failed ? "Stopped" : "Worked"; + active = false; + } else if (activity?.kind === "tool") { + const taxonomy = toolTaxonomy(activity.rawName ?? activity.name); + Icon = taxonomy.icon; + label = taxonomy.verbLabel; + detail = activity.target; + active = activity.status === "pending" || activity.status === "in_progress"; + } else if (activity?.kind === "protocol") { + const presentation = protocolActivityPresentation(activity); + if (presentation) { + Icon = presentation.icon; + label = protocolActivityLabel(activity, presentation); + detail = presentation.detail; + active = protocolActivityIsRunning(activity); + family = activity.surface === "provider_activity" ? activity.family : activity.surface; + } + } + + const semanticLabel = terminal && elapsed + ? `${label} ${terminalStatusFailed(status) ? "after" : "for"} ${elapsed}` + : label; + const showLiveEllipsis = active && Icon !== null; + const expandable = items.length > 0; + const content = ( + <> + {Icon ? ( + + ) : ( + + )} + + + {semanticLabel}{showLiveEllipsis ? "…" : ""} + + {!terminal && elapsed ? · {elapsed} : null} + {detail ? {detail} : null} + + {expandable ? ( + + ) : null} + + ); + + return expandable ? ( + + ) : ( +
+ {content}
); } @@ -49,7 +216,6 @@ export function TaskChatRunnerTurn({ status, startedAtMs, finishedAtMs, - toolSummary, onRuntimeRequestDecision, }: { /** Stable identity used to clear replay-latched final text for the next turn. */ @@ -60,88 +226,72 @@ export function TaskChatRunnerTurn({ status: string; startedAtMs: number | null; finishedAtMs?: number | null; - toolSummary: string | null; onRuntimeRequestDecision?: ( item: TaskChatRuntimeRequestItem, decision: TaskChatRuntimeRequestDecision, ) => void | Promise; }) { - const [open, setOpen] = useState(false); - const historyItems = paperclipRunnerHistoryItems(items); + const terminal = isTerminalRunStatus(status); + const [disclosure, setDisclosure] = useState({ runId, terminal, open: false }); + if (disclosure.runId !== runId || disclosure.terminal !== terminal) { + setDisclosure({ runId, terminal, open: terminal ? false : disclosure.runId === runId && disclosure.open }); + } + const open = disclosure.runId === runId && disclosure.terminal === terminal ? disclosure.open : false; + const controlsId = `task-chat-run-activity-${useId().replaceAll(":", "")}`; + const activityItems = paperclipRunnerActivityItems(items); const pendingRuntimeRequest = lastOf( - historyItems, + items, (item): item is TaskChatRuntimeRequestItem => item.kind === "protocol" && item.surface === "runtime_request" && item.status === "pending", ); - const visibleHistoryItems = pendingRuntimeRequest - ? historyItems.filter((item) => item.id !== pendingRuntimeRequest.id) - : historyItems; - const progress = lastOf(items, (item): item is TaskChatMessageItem => item.kind === "message" && Boolean(item.interstitial)); + const progress = lastOf(activityItems, (item): item is TaskChatMessageItem => item.kind === "message"); const observedFinal = lastOf(items, (item): item is TaskChatMessageItem => item.kind === "message" && item.channel === "final"); // A reconnect/replay can briefly rebuild the transcript without the final // item (or with an earlier, shorter prefix). Once durable answer text is on // screen it must be monotonic for the lifetime of this mounted turn. const finalRef = useRef<{ runId?: string | null; item?: TaskChatMessageItem }>({ runId }); - if (finalRef.current.runId !== runId) { - finalRef.current = { runId }; - } + if (finalRef.current.runId !== runId) finalRef.current = { runId }; if (observedFinal && (!finalRef.current.item || observedFinal.text.length >= finalRef.current.item.text.length)) { finalRef.current.item = observedFinal; } const final = finalRef.current.item; - if (status === "queued") { - return ( -
- {agentName ? : null} - -
- ); - } - return ( -
- setOpen((value) => !value)} - /> +
{agentName ? ( -
+
) : null} -
-
-
- -
-
-
- {!open && progress ? ( + {!open && progress && !final && !terminal ? (
{progress.text}
) : null} + setDisclosure((current) => ({ ...current, open: !current.open }))} + controlsId={controlsId} + /> +
+
+
+ +
+
+
{pendingRuntimeRequest ? (
- +
- ) : !open && !final ? : null} + ) : null} {final ? (
diff --git a/ui/src/components/task-chat/TaskChatThinking.tsx b/ui/src/components/task-chat/TaskChatThinking.tsx index 7d8c5f1fbfb..9036b6f2084 100644 --- a/ui/src/components/task-chat/TaskChatThinking.tsx +++ b/ui/src/components/task-chat/TaskChatThinking.tsx @@ -5,12 +5,32 @@ import { MarkdownBody } from "@/components/MarkdownBody"; import type { TaskChatThinkingItem } from "./task-chat-model"; /** Provider-authored reasoning summaries and deltas; never reconstructed hidden reasoning. */ -export function TaskChatThinking({ item }: { item: TaskChatThinkingItem }) { - const [open, setOpen] = useState(item.streaming || !item.collapsed); +export function TaskChatThinking({ + item, + defaultOpen, + rowClassName, +}: { + item: TaskChatThinkingItem; + /** Activity timelines keep reasoning as one compact row until requested. */ + defaultOpen?: boolean; + /** Optional alignment override for a containing activity rail. */ + rowClassName?: string; +}) { + const [open, setOpen] = useState(defaultOpen ?? (item.streaming || !item.collapsed)); const body = item.lines.join("\n").trim(); - if (!body) return null; const baseLabel = item.channel === "detail" ? "Reasoning detail" : "Reasoning"; - const label = item.streaming ? `${baseLabel}…` : item.summaryLabel ?? baseLabel; + const label = item.streaming + ? body ? `${baseLabel}…` : "Thinking…" + : item.summaryLabel ?? (body ? baseLabel : "Thought"); + + if (!body) { + return ( +
+ + {label} +
+ ); + } return (
@@ -18,11 +38,11 @@ export function TaskChatThinking({ item }: { item: TaskChatThinkingItem }) { type="button" aria-expanded={open} onClick={() => setOpen((value) => !value)} - className="group flex min-w-0 items-center gap-1.5 rounded-sm px-1 py-0.5 text-left text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + className={cn("group/thinking flex min-w-0 items-center gap-1.5 rounded-sm px-1 py-0.5 text-left text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", rowClassName)} > - + {label} - + {open ? (
diff --git a/ui/src/components/task-chat/TaskChatToolCard.tsx b/ui/src/components/task-chat/TaskChatToolCard.tsx index ae962c5fad8..13b4fe311ff 100644 --- a/ui/src/components/task-chat/TaskChatToolCard.tsx +++ b/ui/src/components/task-chat/TaskChatToolCard.tsx @@ -41,7 +41,7 @@ export function TaskChatToolCard({ item }: { item: TaskChatToolItem }) { expandable ? "cursor-pointer transition-colors hover:bg-muted/60 hover:text-foreground" : "cursor-default", )} > - + {item.name} {item.target ? ( {item.target} diff --git a/ui/src/components/task-chat/task-chat-activity-presentation.ts b/ui/src/components/task-chat/task-chat-activity-presentation.ts new file mode 100644 index 00000000000..5063fa38c1f --- /dev/null +++ b/ui/src/components/task-chat/task-chat-activity-presentation.ts @@ -0,0 +1,150 @@ +import { + AlertTriangle, + BookOpen, + Bot, + Box, + Clock3, + Database, + FileCheck2, + FilePenLine, + FileText, + GitBranch, + ListChecks, + PackageCheck, + Search, + ShieldCheck, + TerminalSquare, + Users, + type LucideIcon, +} from "lucide-react"; +import type { + TaskChatMaterializedResourceItem, + TaskChatProtocolItem, + TaskChatProviderActivityItem, + TaskChatWorkspaceChangeItem, + TaskChatWorkspaceFileItem, +} from "./task-chat-model"; + +export interface TaskChatActivityPresentation { + icon: LucideIcon; + runningLabel: string; + completedLabel: string; + failedLabel?: string; + interruptedLabel?: string; + detail?: string; +} + +function providerDetail(item: TaskChatProviderActivityItem, ...labels: string[]): string | undefined { + return item.details.find((entry) => labels.includes(entry.label))?.value; +} + +export function providerActivityPresentation(item: TaskChatProviderActivityItem): TaskChatActivityPresentation { + const detail = item.summary + ?? providerDetail(item, "Query", "URL", "Target", "Name", "Reference", "Reason", "Summary"); + switch (item.family) { + case "research": { + const action = providerDetail(item, "Action")?.toLowerCase(); + if (action === "open_page") { + return { icon: Search, runningLabel: "Opening a web page", completedLabel: "Opened a web page", failedLabel: "Couldn’t open the web page", interruptedLabel: "Stopped opening the web page", detail }; + } + if (action === "find_in_page") { + return { icon: Search, runningLabel: "Searching the page", completedLabel: "Searched the page", failedLabel: "Page search failed", interruptedLabel: "Page search stopped", detail }; + } + return { icon: Search, runningLabel: "Searching the web", completedLabel: "Searched the web", failedLabel: "Web search failed", interruptedLabel: "Web search stopped", detail }; + } + case "tool_execution": + return { icon: TerminalSquare, runningLabel: "Running a tool", completedLabel: "Ran a tool", failedLabel: "Tool failed", interruptedLabel: "Tool stopped", detail }; + case "plan": + return { icon: ListChecks, runningLabel: "Updating the plan", completedLabel: "Updated the plan", failedLabel: "Plan update failed", interruptedLabel: "Plan update stopped", detail }; + case "delegation": { + const action = providerDetail(item, "Action")?.toLowerCase(); + if (action === "message") return { icon: Users, runningLabel: "Messaging a subagent", completedLabel: "Messaged a subagent", failedLabel: "Subagent message failed", interruptedLabel: "Subagent message stopped", detail }; + if (action === "resume") return { icon: Users, runningLabel: "Resuming a subagent", completedLabel: "Resumed a subagent", failedLabel: "Couldn’t resume the subagent", interruptedLabel: "Subagent resume stopped", detail }; + if (action === "close") return { icon: Users, runningLabel: "Closing a subagent", completedLabel: "Closed a subagent", failedLabel: "Couldn’t close the subagent", interruptedLabel: "Subagent close stopped", detail }; + if (action === "wait") return { icon: Users, runningLabel: "Waiting for subagents", completedLabel: "Finished waiting for subagents", failedLabel: "Subagent wait failed", interruptedLabel: "Stopped waiting for subagents", detail }; + return { icon: Users, runningLabel: "Starting a subagent", completedLabel: "Started a subagent", failedLabel: "Subagent start failed", interruptedLabel: "Subagent start stopped", detail }; + } + case "model_identity": + return item.eventType === "model.route.changed" + ? { icon: Bot, runningLabel: "Switching models", completedLabel: "Switched models", failedLabel: "Model switch failed", interruptedLabel: "Model switch stopped", detail } + : { icon: Bot, runningLabel: "Verifying the model", completedLabel: "Verified the model", failedLabel: "Model verification failed", interruptedLabel: "Model verification stopped", detail }; + case "context": + return { icon: Database, runningLabel: "Compacting context", completedLabel: "Compacted context", failedLabel: "Context compaction failed", interruptedLabel: "Context compaction stopped", detail }; + case "artifact": { + const viewed = item.eventType === "artifact.viewed"; + return viewed + ? { icon: Box, runningLabel: "Viewing an artifact", completedLabel: "Viewed an artifact", failedLabel: "Couldn’t view the artifact", interruptedLabel: "Artifact view stopped", detail } + : { icon: Box, runningLabel: "Generating an artifact", completedLabel: "Generated an artifact", failedLabel: "Artifact generation failed", interruptedLabel: "Artifact generation stopped", detail }; + } + case "review": { + const state = providerDetail(item, "State")?.toLowerCase(); + return state === "exited" + ? { icon: FileCheck2, runningLabel: "Leaving review mode", completedLabel: "Left review mode", failedLabel: "Couldn’t leave review mode", interruptedLabel: "Review-mode change stopped", detail } + : { icon: FileCheck2, runningLabel: "Entering review mode", completedLabel: "Entered review mode", failedLabel: "Couldn’t enter review mode", interruptedLabel: "Review-mode change stopped", detail }; + } + case "hook": + return { icon: GitBranch, runningLabel: "Running a hook", completedLabel: "Ran a hook", failedLabel: "Hook failed", interruptedLabel: "Hook stopped", detail }; + case "memory": + return { icon: BookOpen, runningLabel: "Checking memory", completedLabel: "Referenced memory", failedLabel: "Memory lookup failed", interruptedLabel: "Memory lookup stopped", detail }; + case "safety": + return { icon: ShieldCheck, runningLabel: "Reviewing safety", completedLabel: "Reviewed safety", failedLabel: "Safety review failed", interruptedLabel: "Safety review stopped", detail }; + case "terminal": + return { icon: TerminalSquare, runningLabel: "Sending terminal input", completedLabel: "Sent terminal input", failedLabel: "Terminal input failed", interruptedLabel: "Terminal input stopped", detail }; + case "wait": + return { icon: Clock3, runningLabel: "Waiting", completedLabel: "Finished waiting", failedLabel: "Wait failed", interruptedLabel: "Wait stopped", detail }; + case "provider_notice": + return { icon: AlertTriangle, runningLabel: "Provider notice", completedLabel: "Provider notice", failedLabel: "Provider error", interruptedLabel: "Provider notice", detail }; + } +} + +function workspaceChangePresentation(item: TaskChatWorkspaceChangeItem): TaskChatActivityPresentation { + const count = item.totals.files || item.files.length; + const detail = count > 0 ? `${count} ${count === 1 ? "file" : "files"}` : undefined; + return { icon: FilePenLine, runningLabel: "Editing files", completedLabel: "Edited files", detail }; +} + +function workspaceFilePresentation(item: TaskChatWorkspaceFileItem): TaskChatActivityPresentation { + return { + icon: FileText, + runningLabel: "Referencing a file", + completedLabel: "Referenced a file", + detail: item.line == null ? item.path : `${item.path}:${item.line}`, + }; +} + +function resourcePresentation(item: TaskChatMaterializedResourceItem): TaskChatActivityPresentation { + return { + icon: item.resourceKind === "document" ? FileText : PackageCheck, + runningLabel: "Saving a resource", + completedLabel: item.resourceKind === "document" ? "Added a document" : "Added a deliverable", + detail: item.title, + }; +} + +export function protocolActivityPresentation(item: TaskChatProtocolItem): TaskChatActivityPresentation | null { + switch (item.surface) { + case "provider_activity": return providerActivityPresentation(item); + case "workspace_change": return workspaceChangePresentation(item); + case "workspace_file": return workspaceFilePresentation(item); + case "resource": return resourcePresentation(item); + case "runtime_request": + case "run_result": + case "run_terminal": + return null; + } +} + +export function protocolActivityIsRunning(item: TaskChatProtocolItem): boolean { + if (item.surface === "provider_activity") return item.status === "running"; + if (item.surface === "workspace_change") return !item.complete; + return false; +} + +export function protocolActivityLabel(item: TaskChatProtocolItem, presentation: TaskChatActivityPresentation): string { + if (item.surface !== "provider_activity") { + return protocolActivityIsRunning(item) ? presentation.runningLabel : presentation.completedLabel; + } + if (item.status === "failed") return presentation.failedLabel ?? `${presentation.completedLabel} · failed`; + if (item.status === "interrupted") return presentation.interruptedLabel ?? `${presentation.completedLabel} · interrupted`; + return item.status === "running" ? presentation.runningLabel : presentation.completedLabel; +} diff --git a/ui/src/components/task-chat/task-chat-model.ts b/ui/src/components/task-chat/task-chat-model.ts index c12202da917..2e2ec9b5dfa 100644 --- a/ui/src/components/task-chat/task-chat-model.ts +++ b/ui/src/components/task-chat/task-chat-model.ts @@ -137,6 +137,8 @@ export interface TaskChatThinkingItem { summaryLabel?: string; /** Provider-emitted reasoning surface; never synthesized by the UI. */ channel?: "summary" | "detail" | "unknown"; + /** A real provider reasoning lifecycle with no provider-authored text. */ + lifecycleOnly?: boolean; } /** A tool invocation row (ACP tool_call / tool_call_update). */ diff --git a/ui/src/components/task-chat/transcript-adapter.test.ts b/ui/src/components/task-chat/transcript-adapter.test.ts index 9acd5b5a7f9..e1bad8d5191 100644 --- a/ui/src/components/task-chat/transcript-adapter.test.ts +++ b/ui/src/components/task-chat/transcript-adapter.test.ts @@ -3,6 +3,7 @@ import type { TranscriptEntry } from "@/adapters"; import { assembleThreadItems, attachSettledTurns, + buildActivityPhases, buildMergedTurnSummary, buildTurnSummary, coalesceSettledTurns, @@ -10,6 +11,7 @@ import { flattenSelfTalk, isNestableLiveChild, ISSUE_BRIEF_ITEM_ID, + paperclipRunnerActivityItems, paperclipRunnerHistoryItems, prependIssueBrief, settledRunChildren, @@ -200,6 +202,102 @@ describe("transcriptToTaskChatItems protocol surfaces", () => { expect(file && file.surface === "workspace_file" ? file.preview?.length : 0).toBe(8 * 1024); expect(file && file.surface === "workspace_file" ? file.previewTruncated : false).toBe(true); }); + + it("turns canonical research sources into safe links and exposes notice metadata", () => { + const items = transcriptToTaskChatItems([ + { kind: "provider_activity", ts: TS, family: "research", eventType: "research.completed", status: "completed", title: "Research", summary: "PRP", payload: { researchId: "research-1", sources: [{ title: "Protocol notes", url: "https://example.com/prp", snippet: "Canonical event details" }, { title: "Unsafe", url: "file:///etc/passwd" }] } }, + { kind: "provider_activity", ts: TS, family: "provider_notice", eventType: "provider.notice.recorded", status: "informational", title: "Provider notice", summary: "Update configuration", payload: { noticeId: "notice-1", severity: "warning", category: "config", scope: "environment", recoverable: true, userActionable: false } }, + ], opts); + const research = items.find((item) => item.kind === "protocol" && item.surface === "provider_activity" && item.family === "research"); + const notice = items.find((item) => item.kind === "protocol" && item.surface === "provider_activity" && item.family === "provider_notice"); + expect(research?.kind === "protocol" && research.surface === "provider_activity" ? research.links : []).toEqual([{ + label: "Protocol notes", + href: "https://example.com/prp", + description: "Canonical event details", + }]); + expect(notice?.kind === "protocol" && notice.surface === "provider_activity" ? notice.details : []).toEqual(expect.arrayContaining([ + { label: "Severity", value: "warning", mono: false }, + { label: "Category", value: "config", mono: false }, + { label: "Scope", value: "environment", mono: false }, + { label: "Recoverable", value: "Yes", mono: false }, + { label: "User Actionable", value: "No", mono: false }, + ])); + }); + + it("retains every canonical provider family for live and settled rendering", () => { + const families = [ + "plan", "tool_execution", "research", "delegation", "model_identity", "context", "artifact", + "review", "hook", "memory", "safety", "terminal", "wait", "provider_notice", + ] as const; + const entries = families.map((family, index): TranscriptEntry => ({ + kind: "provider_activity", + ts: new Date(Date.parse(TS) + index).toISOString(), + family, + eventType: `${family}.fixture`, + status: index % 2 === 0 ? "running" : "completed", + title: family, + summary: `Visible ${family}`, + payload: { [`${family}Id`]: `${family}-1` }, + })); + const items = transcriptToTaskChatItems(entries, { ...opts, running: true }); + expect(items.map((item) => item.kind === "protocol" && item.surface === "provider_activity" ? item.family : null)) + .toEqual(families); + }); +}); + +describe("buildActivityPhases provider summaries", () => { + it("includes canonical provider work in the folded activity summary", () => { + const provider = (id: string, family: "research" | "hook"): TaskChatItem => ({ + id, + kind: "protocol", + surface: "provider_activity", + family, + eventType: `${family}.completed`, + status: "completed", + title: family, + details: [], + steps: [], + links: [], + children: [], + }); + const phases = buildActivityPhases([ + { id: "finish", kind: "tool", name: "Paperclip_finish", status: "completed" }, + provider("research-1", "research"), + provider("research-2", "research"), + provider("research-3", "research"), + provider("hook-1", "hook"), + { + id: "workspace", + kind: "protocol", + surface: "workspace_change", + changeSetId: "change-1", + revision: 1, + source: "runner_verified", + complete: true, + files: [], + totals: { files: 2, additions: 1, deletions: 1 }, + patchArtifactRef: null, + }, + ], false); + expect(phases[0]?.summary).toBe("Called 1 tool, Searched 3 times, Ran a hook, Changed 2 files"); + }); + + it("counts canonical tool execution when no native tool row exists", () => { + const phases = buildActivityPhases([{ + id: "provider-tool", + kind: "protocol", + surface: "provider_activity", + family: "tool_execution", + eventType: "tool.execution.completed", + status: "completed", + title: "Tool execution", + details: [], + steps: [], + links: [], + children: [], + }], false); + expect(phases[0]?.summary).toBe("Ran a tool"); + }); }); describe("buildTurnSummary tool counting", () => { @@ -448,6 +546,21 @@ describe("settledRunChildren (PAP-361)", () => { }); describe("paperclip runner semantic channels", () => { + it("turns empty reasoning lifecycle frames into a visible thinking item", () => { + const parsed = transcriptToTaskChatItems([ + { kind: "thinking", ts: "2026-08-21T12:00:00.000Z", text: "", lifecycle: "started", channel: "summary" }, + { kind: "thinking", ts: "2026-08-21T12:00:02.000Z", text: "", lifecycle: "completed", channel: "summary" }, + ], { runId: "run-empty-reasoning", running: true }); + + expect(parsed).toEqual([expect.objectContaining({ + kind: "thinking", + lines: [], + streaming: false, + lifecycleOnly: true, + summaryLabel: "Thought for 2s", + })]); + }); + it("keeps progress/final messages and reasoning summary/detail in distinct items", () => { const parsed = transcriptToTaskChatItems([ { kind: "assistant", ts: TS, text: "Checking.", channel: "progress", delta: true }, @@ -482,6 +595,61 @@ describe("paperclip runner semantic channels", () => { ]); expect(items).toHaveLength(7); }); + + it("projects one semantic activity row per useful logical item", () => { + const items: TaskChatItem[] = [ + { id: "progress", kind: "message", author: "agent", text: "Looking this up.", interstitial: true, channel: "progress" }, + { id: "final", kind: "message", author: "agent", text: "Finished.", channel: "final" }, + { id: "empty-thinking", kind: "thinking", lines: [], lifecycleOnly: true }, + { id: "thinking", kind: "thinking", lines: ["Provider-authored summary"] }, + { id: "tool", kind: "tool", name: "Search", rawName: "web_search", status: "completed" }, + { id: "finish", kind: "tool", name: "Paperclip_finish", rawName: "paperclip_finish", status: "completed" }, + { id: "session", kind: "marker", variant: "session_start", label: "Session started" }, + { id: "interrupt", kind: "marker", variant: "interrupted", label: "Interrupted" }, + { id: "usage", kind: "usage", usage: { used: 20, size: 100 } }, + { + id: "research", + kind: "protocol", + surface: "provider_activity", + family: "research", + eventType: "research.completed", + status: "completed", + title: "Research", + details: [], + steps: [], + links: [], + children: [], + }, + { + id: "result", + kind: "protocol", + surface: "run_result", + disposition: "done", + summary: "Finished.", + objectiveSatisfied: true, + verification: [], + remainingWork: [], + blocker: null, + artifacts: [], + }, + { + id: "terminal", + kind: "protocol", + surface: "run_terminal", + turnState: "completed", + runState: "succeeded", + disposition: "done", + }, + ]; + + expect(paperclipRunnerActivityItems(items).map((item) => item.id)).toEqual([ + "progress", + "thinking", + "tool", + "interrupt", + "research", + ]); + }); }); describe("buildMergedTurnSummary (PAP-362)", () => { diff --git a/ui/src/components/task-chat/transcript-adapter.ts b/ui/src/components/task-chat/transcript-adapter.ts index 7c48117388c..0c7197d9607 100644 --- a/ui/src/components/task-chat/transcript-adapter.ts +++ b/ui/src/components/task-chat/transcript-adapter.ts @@ -161,7 +161,10 @@ function scalarValue(value: unknown): string | undefined { } function titleCaseKey(value: string): string { - return value.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); + return value + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replaceAll("_", " ") + .replace(/\b\w/g, (letter) => letter.toUpperCase()); } function safeHttpHref(value: unknown): string | null { @@ -177,19 +180,19 @@ function safeHttpHref(value: unknown): string | null { const PROVIDER_DETAIL_KEYS: Record = { plan: ["revision", "syncStatus", "documentRevision", "complete"], - tool_execution: ["transport", "operation", "name", "target", "progress", "durationMs", "exitCode", "outputBytes"], - research: ["action", "query", "pattern", "url"], - delegation: ["action"], + tool_execution: ["transport", "operation", "name", "target", "namespace", "readOnly", "status", "progress", "durationMs", "exitCode", "outputBytes"], + research: ["action", "status", "query", "pattern", "url"], + delegation: ["action", "status"], model_identity: ["provider", "requestedModel", "fromModel", "effectiveModel", "reason", "status", "buffering", "summary"], context: ["reason", "preTokens", "postTokens", "sameSession"], artifact: ["status", "reference", "mediaType", "title", "registered", "transparentBackground", "failure"], review: ["state", "scope"], hook: ["event", "scope", "status", "blocking", "durationMs", "summary"], memory: ["label", "available", "reference"], - safety: ["status", "decision", "summary"], + safety: ["status", "decision", "targetExecutionId", "summary"], terminal: ["origin", "inputClass", "byteCount"], wait: ["reason", "status", "plannedDurationMs", "elapsedDurationMs"], - provider_notice: ["level", "code", "message", "action"], + provider_notice: ["severity", "category", "scope", "recoverable", "userActionable", "summary"], }; function providerActivityKey(entry: Extract): string { @@ -327,25 +330,30 @@ export function transcriptToTaskChatItems( for (const [i, entry] of entries.entries()) { switch (entry.kind) { case "thinking": { - if (!entry.text) break; + const lifecycleOnly = !entry.text; if (thinkingIndex >= 0 && thinkingChannel === entry.channel) { const it = items[thinkingIndex]; if (it.kind === "thinking") { - it.lines.push(...entry.text.split("\n")); + if (entry.text) { + it.lines.push(...entry.text.split("\n")); + it.lifecycleOnly = false; + } const startTs = thinkingStartTs.get(thinkingIndex); const label = thoughtDurationLabel(startTs, entry.ts); if (label) it.summaryLabel = label; + if (entry.lifecycle === "completed") it.streaming = false; } } else { items.push({ id: `${runId}:think:${i}`, kind: "thinking", - lines: entry.text.split("\n"), - streaming: running, + lines: entry.text ? entry.text.split("\n") : [], + streaming: running && entry.lifecycle !== "completed", // Settled history folds its thinking behind the header (v7); // the in-flight run streams it expanded. collapsed: !running, channel: entry.channel, + lifecycleOnly, }); thinkingIndex = items.length - 1; thinkingChannel = entry.channel; @@ -353,6 +361,12 @@ export function transcriptToTaskChatItems( messageIndex = -1; messageChannel = undefined; } + if (entry.lifecycle === "completed") { + const it = items[thinkingIndex]; + if (it?.kind === "thinking") it.streaming = false; + thinkingIndex = -1; + thinkingChannel = undefined; + } break; } case "assistant": { @@ -694,10 +708,62 @@ export function paperclipRunnerHistoryItems(parsed: readonly TaskChatItem[]): Ta }); } +/** + * Semantic rows for the Paperclip runner's Codex-style activity disclosure. + * + * This is intentionally narrower than the stored transcript and the runner + * inspector: it keeps work a person can understand or act on while excluding + * transport/lifecycle bookkeeping, usage, terminal authority, and the final + * response. Logical provider/tool lifecycles have already been coalesced by + * `transcriptToTaskChatItems`, so each returned item is one visible "thing". + */ +export function paperclipRunnerActivityItems(parsed: readonly TaskChatItem[]): TaskChatItem[] { + return parsed.filter((item) => { + switch (item.kind) { + case "message": + return Boolean(item.interstitial && item.text.trim()); + case "thinking": + // A textless reasoning lifecycle drives the live "Thinking…" label, + // but repeating it as an empty child row adds no information. + return item.lines.some((line) => line.trim().length > 0); + case "tool": { + const normalizedName = (item.rawName ?? item.name).replaceAll("-", "_").toLowerCase(); + return normalizedName !== "paperclip_finish"; + } + case "marker": + return item.variant === "interrupted"; + case "protocol": + return item.surface === "provider_activity" + || item.surface === "workspace_change" + || item.surface === "workspace_file" + || item.surface === "resource"; + case "usage": + case "status": + case "activity_phase": + case "interaction": + case "turn": + case "brief": + return false; + } + }); +} + function phaseSummary(items: readonly (TaskChatToolItem | { kind: "usage" } | { kind: "thinking" } | { kind: "marker" } | TaskChatProtocolItem)[]): string { const counts = new Map(); + const providerCounts = new Map(); let generic = 0; + let workspaceFiles = 0; for (const item of items) { + if (item.kind === "protocol") { + if (item.surface === "provider_activity") { + providerCounts.set(item.family, (providerCounts.get(item.family) ?? 0) + 1); + } else if (item.surface === "workspace_change") { + workspaceFiles += item.totals.files || item.files.length; + } else if (item.surface === "workspace_file") { + workspaceFiles += 1; + } + continue; + } if (item.kind !== "tool") continue; if (isGenericToolName(item.rawName ?? item.name)) { generic += 1; @@ -719,6 +785,31 @@ function phaseSummary(items: readonly (TaskChatToolItem | { kind: "usage" } | { const known = new Set(["read", "edit", "terminal", "grep", "search"]); const other = [...counts].reduce((n, [family, count]) => n + (known.has(family) ? 0 : count), 0) + generic; if (other) phrases.push(`Called ${other} ${other === 1 ? "tool" : "tools"}`); + const providerCount = (family: TaskChatProviderActivityItem["family"]) => providerCounts.get(family) ?? 0; + const addProvider = (family: TaskChatProviderActivityItem["family"], singular: string, plural: (count: number) => string) => { + const count = providerCount(family); + if (count) phrases.push(count === 1 ? singular : plural(count)); + }; + addProvider("plan", "Updated the plan", (count) => `Updated ${count} plans`); + addProvider("research", "Searched once", (count) => `Searched ${count} times`); + addProvider("delegation", "Used a subagent", (count) => `Used ${count} subagents`); + addProvider("model_identity", "Updated the model", (count) => `Updated the model ${count} times`); + addProvider("context", "Compacted context", (count) => `Compacted context ${count} times`); + addProvider("artifact", "Handled an artifact", (count) => `Handled ${count} artifacts`); + addProvider("review", "Changed review mode", (count) => `Changed review mode ${count} times`); + addProvider("hook", "Ran a hook", (count) => `Ran ${count} hooks`); + addProvider("memory", "Referenced memory", (count) => `Referenced memory ${count} times`); + addProvider("safety", "Ran a safety review", (count) => `Ran ${count} safety reviews`); + addProvider("terminal", "Sent terminal input", (count) => `Sent terminal input ${count} times`); + addProvider("wait", "Waited", (count) => `Waited ${count} times`); + addProvider("provider_notice", "Received a provider notice", (count) => `Received ${count} provider notices`); + // A canonical tool-execution row can be the only tool representation for a + // provider. Avoid double-counting when the adapter also produced a native + // TaskChatToolItem for the same execution. + if ([...counts.values()].reduce((total, count) => total + count, generic) === 0) { + addProvider("tool_execution", "Ran a tool", (count) => `Ran ${count} tools`); + } + if (workspaceFiles > 0) phrases.push(`Changed ${workspaceFiles} ${workspaceFiles === 1 ? "file" : "files"}`); if (phrases.length > 0) return phrases.join(", "); const protocolCount = items.filter((item) => item.kind === "protocol").length; if (protocolCount > 0) return protocolCount === 1 ? "Runner activity" : `${protocolCount} runner updates`; diff --git a/ui/src/index.css b/ui/src/index.css index 2bc7bc81de0..05f0f8b0a47 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -683,6 +683,11 @@ .tc-turn-fold[data-folded="true"] { grid-template-rows: 0fr; opacity: 0; } .tc-turn-fold > * { overflow: hidden; min-height: 0; } +/* Hover-revealed activity carets remain discoverable on touch-only devices. */ +@media (hover: none) { + .tc-hover-disclosure-caret { opacity: 1; } +} + /* Properties-pane maximize/restore glide (left animates between docked and sidebar-flush while the pane is position:fixed). */ .tc-pane-glide { transition: left var(--motion-pane-glide) var(--motion-ease-standard); } @@ -2112,6 +2117,8 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] { --sz-52vh: 52vh; --sz-40rem: 40rem; --sz-44rem: 44rem; + --sz-36rem: 36rem; + --sz-neg-1_25rem: -1.25rem; --sz-30pct: 30%; --sz-24pct: 24%; --sz-18pct: 18%; @@ -2345,6 +2352,12 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] { --gtc-63: 280px 1fr; --gtc-64: minmax(0,20rem) 1fr; --gtc-65: minmax(8rem,0.38fr) minmax(0,0.62fr) auto; + --gtc-runner-inspector-stage: 1.25rem minmax(0,1fr); + --gtc-runner-inspector-filters: minmax(14rem,1fr) repeat(5,minmax(8rem,auto)); + --gtc-runner-inspector-pipeline: 21rem minmax(0,1fr); + --gtc-runner-inspector-fields: 8rem 1fr; + --gtc-runner-inspector-presentation: minmax(18rem,0.8fr) minmax(20rem,1.2fr); + --gtc-runner-inspector-trace: 20rem minmax(0,1fr); --gtr-1: auto auto; /* Extracted from ui/src/components/ui/card.tsx (grid-rows-[auto_auto]). */ --gtr-2: 1fr; /* Extracted from ui/src/pages/CompanySkills.tsx (grid-rows-[1fr]). */ --gtr-3: 0fr; /* Extracted from ui/src/pages/CompanySkills.tsx (grid-rows-[0fr]). */ diff --git a/ui/src/lib/agent-config-patch.test.ts b/ui/src/lib/agent-config-patch.test.ts index 46e6e858290..9fac4e16d5c 100644 --- a/ui/src/lib/agent-config-patch.test.ts +++ b/ui/src/lib/agent-config-patch.test.ts @@ -52,12 +52,27 @@ function makeOverlay(patch?: Partial): AgentConfigOverlay { identity: {}, adapterConfig: {}, heartbeat: {}, + debug: {}, runtime: {}, ...patch, }; } describe("buildAgentUpdatePatch", () => { + it("merges the agent-scoped provider trace debug setting into runtime config", () => { + const patch = buildAgentUpdatePatch( + makeAgent(), + makeOverlay({ debug: { providerTrace: "raw" } }), + ); + + expect(patch).toMatchObject({ + runtimeConfig: { + heartbeat: { enabled: true, intervalSec: 300 }, + debug: { providerTrace: "raw" }, + }, + }); + }); + it("replaces adapter config and drops env when the last env binding is cleared", () => { const patch = buildAgentUpdatePatch( makeAgent(), diff --git a/ui/src/lib/agent-config-patch.ts b/ui/src/lib/agent-config-patch.ts index cc4d4f1e302..f5f90de8d91 100644 --- a/ui/src/lib/agent-config-patch.ts +++ b/ui/src/lib/agent-config-patch.ts @@ -15,6 +15,7 @@ export interface AgentConfigOverlay { adapterType?: string; adapterConfig: Record; heartbeat: Record; + debug: Record; runtime: Record; modelProfiles?: { cheap?: AgentModelProfileOverlay }; } @@ -60,7 +61,11 @@ export function buildAgentUpdatePatch(agent: Agent, overlay: AgentConfigOverlay) const cheapOverlay = overlay.modelProfiles?.cheap; const hasModelProfileChange = cheapOverlay !== undefined; - if (Object.keys(overlay.heartbeat).length > 0 || hasModelProfileChange) { + if ( + Object.keys(overlay.heartbeat).length > 0 + || Object.keys(overlay.debug).length > 0 + || hasModelProfileChange + ) { const existingRc = (agent.runtimeConfig ?? {}) as Record; const nextRuntimeConfig: Record = (patch.runtimeConfig as Record | undefined) ?? { ...existingRc }; @@ -70,6 +75,16 @@ export function buildAgentUpdatePatch(agent: Agent, overlay: AgentConfigOverlay) nextRuntimeConfig.heartbeat = { ...existingHb, ...overlay.heartbeat }; } + if (Object.keys(overlay.debug).length > 0) { + const existingDebug = (existingRc.debug ?? {}) as Record; + const nextDebug = omitUndefinedEntries({ ...existingDebug, ...overlay.debug }); + if (Object.keys(nextDebug).length === 0) { + delete nextRuntimeConfig.debug; + } else { + nextRuntimeConfig.debug = nextDebug; + } + } + if (hasModelProfileChange) { const existingProfiles = ((existingRc.modelProfiles ?? {}) as Record); const existingCheap = ((existingProfiles.cheap ?? {}) as Record); diff --git a/ui/src/lib/issue-chat-messages.ts b/ui/src/lib/issue-chat-messages.ts index 37d96ac5873..aad2bd4a741 100644 --- a/ui/src/lib/issue-chat-messages.ts +++ b/ui/src/lib/issue-chat-messages.ts @@ -35,6 +35,10 @@ export interface IssueChatComment extends IssueComment { queueTargetRunId?: string | null; queueReason?: "hold" | "active_run" | "other"; followUpRequested?: boolean; + /** Causal conversation slot: the run that actually consumed this input. */ + consumedByRunId?: string | null; + conversationAnchorAt?: Date | string | null; + conversationAnchorSequence?: number; } export interface IssueChatLinkedRun { diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index c21dba53c58..32052f8f0f4 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -14,46 +14,72 @@ export const queryKeys = { * asked. A different account is a different entry, so there is nothing to * mistake. `null` is the signed-out/local_trusted key. */ - list: (userId: string | null) => ["companies", "list", userId ?? "anonymous"] as const, + list: (userId: string | null) => + ["companies", "list", userId ?? "anonymous"] as const, detail: (id: string) => ["companies", id] as const, stats: ["companies", "stats"] as const, - exportFidelity: (companyId: string) => ["companies", companyId, "export-fidelity"] as const, + exportFidelity: (companyId: string) => + ["companies", companyId, "export-fidelity"] as const, }, apps: { gallery: (companyId: string) => ["apps", companyId, "gallery"] as const, attention: (companyId: string) => ["apps", companyId, "attention"] as const, }, tools: { - applications: (companyId: string) => ["tools", companyId, "applications"] as const, - connections: (companyId: string) => ["tools", companyId, "connections"] as const, - connection: (connectionId: string) => ["tools", "connection", connectionId] as const, + applications: (companyId: string) => + ["tools", companyId, "applications"] as const, + connections: (companyId: string) => + ["tools", companyId, "connections"] as const, + connection: (connectionId: string) => + ["tools", "connection", connectionId] as const, connectionInstalls: (connectionId: string) => ["tools", "connection", connectionId, "installs"] as const, - catalog: (connectionId: string) => ["tools", "connection", connectionId, "catalog"] as const, + catalog: (connectionId: string) => + ["tools", "connection", connectionId, "catalog"] as const, connectionActivity: (connectionId: string) => ["tools", "connection", connectionId, "activity"] as const, testAgents: (connectionId: string) => ["tools", "connection", connectionId, "test-agents"] as const, testCallStatus: (connectionId: string, actionRequestId: string) => - ["tools", "connection", connectionId, "test-calls", actionRequestId] as const, + [ + "tools", + "connection", + connectionId, + "test-calls", + actionRequestId, + ] as const, actionRequests: (companyId: string, status: string) => ["tools", companyId, "action-requests", status] as const, gateways: (companyId: string) => ["tools", "gateways", companyId] as const, profiles: (companyId: string) => ["tools", companyId, "profiles"] as const, - profileNewTools: (profileId: string) => ["tools", "profiles", profileId, "new-tools"] as const, + profileNewTools: (profileId: string) => + ["tools", "profiles", profileId, "new-tools"] as const, effectiveProfilesForAgent: (companyId: string, agentId: string) => ["tools", companyId, "profiles", "effective", "agent", agentId] as const, - stdioTemplates: (companyId: string) => ["tools", companyId, "stdio-templates"] as const, - runtimeSlots: (companyId: string) => ["tools", companyId, "runtime-slots"] as const, - runtimeHealth: (companyId: string) => ["tools", companyId, "runtime-health"] as const, - runDecisions: (companyId: string, runId: string) => ["tools", companyId, "runs", runId, "decisions"] as const, - liveRuntimeSlots: (companyId: string) => ["tools", companyId, "runtime-slots", "live"] as const, + stdioTemplates: (companyId: string) => + ["tools", companyId, "stdio-templates"] as const, + runtimeSlots: (companyId: string) => + ["tools", companyId, "runtime-slots"] as const, + runtimeHealth: (companyId: string) => + ["tools", companyId, "runtime-health"] as const, + runDecisions: (companyId: string, runId: string) => + ["tools", companyId, "runs", runId, "decisions"] as const, + liveRuntimeSlots: (companyId: string) => + ["tools", companyId, "runtime-slots", "live"] as const, policies: (companyId: string) => ["tools", companyId, "policies"] as const, - trustRules: (companyId: string) => ["tools", companyId, "trust-rules"] as const, - audit: (companyId: string, limit: number) => ["tools", companyId, "audit", limit] as const, + trustRules: (companyId: string) => + ["tools", companyId, "trust-rules"] as const, + audit: (companyId: string, limit: number) => + ["tools", companyId, "audit", limit] as const, activity: ( companyId: string, - filters: { app?: string; agent?: string; outcome?: string; window?: string; search?: string }, + filters: { + app?: string; + agent?: string; + outcome?: string; + window?: string; + search?: string; + }, ) => [ "tools", @@ -97,17 +123,22 @@ export const queryKeys = { ] as const, }, smokeLab: { - services: (companyId: string) => ["smoke-lab", companyId, "services"] as const, + services: (companyId: string) => + ["smoke-lab", companyId, "services"] as const, runs: (companyId: string) => ["smoke-lab", companyId, "runs"] as const, - run: (companyId: string, runId: string) => ["smoke-lab", companyId, "runs", runId] as const, + run: (companyId: string, runId: string) => + ["smoke-lab", companyId, "runs", runId] as const, }, companySkills: { list: (companyId: string) => ["company-skills", companyId] as const, listRecent: (companyId: string) => ["company-skills", companyId, "recent-updated"] as const, - detail: (companyId: string, skillId: string) => ["company-skills", companyId, skillId] as const, - versions: (companyId: string, skillId: string) => ["company-skills", companyId, skillId, "versions"] as const, - comments: (companyId: string, skillId: string) => ["company-skills", companyId, skillId, "comments"] as const, + detail: (companyId: string, skillId: string) => + ["company-skills", companyId, skillId] as const, + versions: (companyId: string, skillId: string) => + ["company-skills", companyId, skillId, "versions"] as const, + comments: (companyId: string, skillId: string) => + ["company-skills", companyId, skillId, "comments"] as const, updateStatus: (companyId: string, skillId: string) => ["company-skills", companyId, skillId, "update-status"] as const, forkPrecheck: (companyId: string, skillId: string) => @@ -115,8 +146,15 @@ export const queryKeys = { file: (companyId: string, skillId: string, relativePath: string) => ["company-skills", companyId, skillId, "file", relativePath] as const, catalog: (filters: { kind?: string; category?: string; q?: string } = {}) => - ["company-skills", "catalog", filters.kind ?? "__all-kinds__", filters.category ?? "__all-categories__", filters.q ?? ""] as const, - catalogDetail: (catalogRef: string) => ["company-skills", "catalog", "detail", catalogRef] as const, + [ + "company-skills", + "catalog", + filters.kind ?? "__all-kinds__", + filters.category ?? "__all-categories__", + filters.q ?? "", + ] as const, + catalogDetail: (catalogRef: string) => + ["company-skills", "catalog", "detail", catalogRef] as const, catalogFile: (catalogRef: string, relativePath: string) => ["company-skills", "catalog", "file", catalogRef, relativePath] as const, testInputs: (companyId: string, skillId: string) => @@ -124,17 +162,31 @@ export const queryKeys = { testRunTemplates: (companyId: string) => ["company-skills", companyId, "test-run-templates"] as const, testRuns: (companyId: string, skillId: string, inputId?: string | null) => - ["company-skills", companyId, skillId, "test-runs", inputId ?? "__all-inputs__"] as const, + [ + "company-skills", + companyId, + skillId, + "test-runs", + inputId ?? "__all-inputs__", + ] as const, testRunDetail: (companyId: string, skillId: string, runId: string) => ["company-skills", companyId, skillId, "test-run", runId] as const, }, teamCatalog: { catalog: (filters: { kind?: string; category?: string; q?: string } = {}) => - ["team-catalog", "catalog", filters.kind ?? "__all-kinds__", filters.category ?? "__all-categories__", filters.q ?? ""] as const, - catalogDetail: (catalogRef: string) => ["team-catalog", "catalog", "detail", catalogRef] as const, + [ + "team-catalog", + "catalog", + filters.kind ?? "__all-kinds__", + filters.category ?? "__all-categories__", + filters.q ?? "", + ] as const, + catalogDetail: (catalogRef: string) => + ["team-catalog", "catalog", "detail", catalogRef] as const, catalogFile: (catalogRef: string, relativePath: string) => ["team-catalog", "catalog", "file", catalogRef, relativePath] as const, - installed: (companyId: string) => ["team-catalog", "installed", companyId] as const, + installed: (companyId: string) => + ["team-catalog", "installed", companyId] as const, }, agents: { list: (companyId: string) => ["agents", companyId] as const, @@ -142,13 +194,25 @@ export const queryKeys = { runtimeState: (id: string) => ["agents", "runtime-state", id] as const, taskSessions: (id: string) => ["agents", "task-sessions", id] as const, skills: (id: string) => ["agents", "skills", id] as const, - instructionsBundle: (id: string) => ["agents", "instructions-bundle", id] as const, + instructionsBundle: (id: string) => + ["agents", "instructions-bundle", id] as const, instructionsFile: (id: string, relativePath: string) => ["agents", "instructions-bundle", id, "file", relativePath] as const, keys: (agentId: string) => ["agents", "keys", agentId] as const, - configRevisions: (agentId: string) => ["agents", "config-revisions", agentId] as const, - adapterModels: (companyId: string, adapterType: string, environmentId?: string | null) => - ["agents", companyId, "adapter-models", adapterType, environmentId ?? null] as const, + configRevisions: (agentId: string) => + ["agents", "config-revisions", agentId] as const, + adapterModels: ( + companyId: string, + adapterType: string, + environmentId?: string | null, + ) => + [ + "agents", + companyId, + "adapter-models", + adapterType, + environmentId ?? null, + ] as const, adapterModelProfiles: (companyId: string, adapterType: string) => ["agents", companyId, "adapter-model-profiles", adapterType] as const, detectModel: (companyId: string, adapterType: string) => @@ -158,66 +222,140 @@ export const queryKeys = { list: (companyId: string) => ["built-in-agents", companyId] as const, }, summarySlots: { - detail: (companyId: string, scopeKind: string, slotKey: string, scopeId?: string | null) => - ["summary-slots", companyId, scopeKind, slotKey, scopeId ?? null] as const, - revisions: (companyId: string, scopeKind: string, slotKey: string, scopeId?: string | null) => - ["summary-slots", companyId, scopeKind, slotKey, scopeId ?? null, "revisions"] as const, + detail: ( + companyId: string, + scopeKind: string, + slotKey: string, + scopeId?: string | null, + ) => + [ + "summary-slots", + companyId, + scopeKind, + slotKey, + scopeId ?? null, + ] as const, + revisions: ( + companyId: string, + scopeKind: string, + slotKey: string, + scopeId?: string | null, + ) => + [ + "summary-slots", + companyId, + scopeKind, + slotKey, + scopeId ?? null, + "revisions", + ] as const, }, statusCards: { list: (companyId: string, archived: boolean) => ["status-cards", companyId, archived ? "archived" : "active"] as const, detail: (id: string) => ["status-cards", "detail", id] as const, updates: (id: string) => ["status-cards", "detail", id, "updates"] as const, - summaryRevisions: (id: string) => ["status-cards", "detail", id, "summary-revisions"] as const, + summaryRevisions: (id: string) => + ["status-cards", "detail", id, "summary-revisions"] as const, dryRun: (id: string) => ["status-cards", "detail", id, "dry-run"] as const, }, issues: { list: (companyId: string) => ["issues", companyId] as const, - mentionPool: (companyId: string) => ["issues", companyId, "mention-pool"] as const, - search: (companyId: string, q: string, projectId?: string, limit?: number) => - ["issues", companyId, "search", q, projectId ?? "__all-projects__", limit ?? "__no-limit__"] as const, - listAssignedToMe: (companyId: string) => ["issues", companyId, "assigned-to-me"] as const, - listMineByMe: (companyId: string) => ["issues", companyId, "mine-by-me"] as const, - listTouchedByMe: (companyId: string) => ["issues", companyId, "touched-by-me"] as const, - listUnreadTouchedByMe: (companyId: string) => ["issues", companyId, "unread-touched-by-me"] as const, - listBlockedAttention: (companyId: string) => ["issues", companyId, "blocked-attention"] as const, - countBlockedAttention: (companyId: string) => ["issues", companyId, "blocked-attention", "count"] as const, + mentionPool: (companyId: string) => + ["issues", companyId, "mention-pool"] as const, + search: ( + companyId: string, + q: string, + projectId?: string, + limit?: number, + ) => + [ + "issues", + companyId, + "search", + q, + projectId ?? "__all-projects__", + limit ?? "__no-limit__", + ] as const, + listAssignedToMe: (companyId: string) => + ["issues", companyId, "assigned-to-me"] as const, + listMineByMe: (companyId: string) => + ["issues", companyId, "mine-by-me"] as const, + listTouchedByMe: (companyId: string) => + ["issues", companyId, "touched-by-me"] as const, + listUnreadTouchedByMe: (companyId: string) => + ["issues", companyId, "unread-touched-by-me"] as const, + listBlockedAttention: (companyId: string) => + ["issues", companyId, "blocked-attention"] as const, + countBlockedAttention: (companyId: string) => + ["issues", companyId, "blocked-attention", "count"] as const, labels: (companyId: string) => ["issues", companyId, "labels"] as const, listByProject: (companyId: string, projectId: string) => ["issues", companyId, "project", projectId] as const, - listPluginOperationsByProject: (companyId: string, projectId: string, originKindPrefix: string) => - ["issues", companyId, "project", projectId, "plugin-operations", originKindPrefix] as const, + listPluginOperationsByProject: ( + companyId: string, + projectId: string, + originKindPrefix: string, + ) => + [ + "issues", + companyId, + "project", + projectId, + "plugin-operations", + originKindPrefix, + ] as const, listByParent: (companyId: string, parentId: string) => ["issues", companyId, "parent", parentId] as const, listByDescendantRoot: (companyId: string, rootIssueId: string) => ["issues", companyId, "descendants", rootIssueId] as const, - listByExecutionWorkspace: (companyId: string, executionWorkspaceId: string) => - ["issues", companyId, "execution-workspace", executionWorkspaceId] as const, + listByExecutionWorkspace: ( + companyId: string, + executionWorkspaceId: string, + ) => + [ + "issues", + companyId, + "execution-workspace", + executionWorkspaceId, + ] as const, detail: (id: string) => ["issues", "detail", id] as const, comments: (issueId: string) => ["issues", "comments", issueId] as const, - queuedComments: (issueId: string) => ["issues", "queued-comments", issueId] as const, - commentsList: (issueId: string) => ["issues", "comments", issueId, "list"] as const, - interactions: (issueId: string) => ["issues", "interactions", issueId] as const, + commentsList: (issueId: string) => + ["issues", "comments", issueId, "list"] as const, + queuedComments: (issueId: string) => + ["issues", "queued-comments", issueId] as const, + interactions: (issueId: string) => + ["issues", "interactions", issueId] as const, acceptedPlanDecompositions: (issueId: string) => ["issues", "accepted-plan-decompositions", issueId] as const, - feedbackVotes: (issueId: string) => ["issues", "feedback-votes", issueId] as const, + feedbackVotes: (issueId: string) => + ["issues", "feedback-votes", issueId] as const, costSummary: (issueId: string, options: { excludeRoot?: boolean } = {}) => options.excludeRoot ? (["issues", "cost-summary", issueId, "exclude-root"] as const) : (["issues", "cost-summary", issueId] as const), - attachments: (issueId: string) => ["issues", "attachments", issueId] as const, - attachmentPreview: (attachmentId: string) => ["issues", "attachment-preview", attachmentId] as const, + attachments: (issueId: string) => + ["issues", "attachments", issueId] as const, + attachmentPreview: (attachmentId: string) => + ["issues", "attachment-preview", attachmentId] as const, documents: (issueId: string) => ["issues", "documents", issueId] as const, - document: (issueId: string, key: string) => ["issues", "document", issueId, key] as const, - documentRevisions: (issueId: string, key: string) => ["issues", "document-revisions", issueId, key] as const, - documentAnnotations: (issueId: string, key: string, status: "open" | "resolved" | "all" = "all") => - ["issues", "document-annotations", issueId, key, status] as const, + document: (issueId: string, key: string) => + ["issues", "document", issueId, key] as const, + documentRevisions: (issueId: string, key: string) => + ["issues", "document-revisions", issueId, key] as const, + documentAnnotations: ( + issueId: string, + key: string, + status: "open" | "resolved" | "all" = "all", + ) => ["issues", "document-annotations", issueId, key, status] as const, activity: (issueId: string) => ["issues", "activity", issueId] as const, runs: (issueId: string) => ["issues", "runs", issueId] as const, approvals: (issueId: string) => ["issues", "approvals", issueId] as const, liveRuns: (issueId: string) => ["issues", "live-runs", issueId] as const, activeRun: (issueId: string) => ["issues", "active-run", issueId] as const, - workProducts: (issueId: string) => ["issues", "work-products", issueId] as const, + workProducts: (issueId: string) => + ["issues", "work-products", issueId] as const, fileResources: ( issueId: string, options: { @@ -230,18 +368,25 @@ export const queryKeys = { limit?: number; offset?: number; } = {}, - ) => - ["issues", "file-resources", issueId, "list", options] as const, + ) => ["issues", "file-resources", issueId, "list", options] as const, fileResource: ( issueId: string, - query: { path: string; workspace?: string; projectId?: string | null; workspaceId?: string | null }, - ) => - ["issues", "file-resources", issueId, "resolve", query] as const, + query: { + path: string; + workspace?: string; + projectId?: string | null; + workspaceId?: string | null; + }, + ) => ["issues", "file-resources", issueId, "resolve", query] as const, fileResourceContent: ( issueId: string, - query: { path: string; workspace?: string; projectId?: string | null; workspaceId?: string | null }, - ) => - ["issues", "file-resources", issueId, "content", query] as const, + query: { + path: string; + workspace?: string; + projectId?: string | null; + workspaceId?: string | null; + }, + ) => ["issues", "file-resources", issueId, "content", query] as const, /** * Batched availability preflight. `refKeys` are the deduplicated, * lexicographically sorted reference keys in the request so identical @@ -252,52 +397,83 @@ export const queryKeys = { }, routines: { list: (companyId: string, filters?: { projectId?: string | null }) => - ["routines", companyId, filters?.projectId ?? "__all-projects__"] as const, + [ + "routines", + companyId, + filters?.projectId ?? "__all-projects__", + ] as const, detail: (id: string) => ["routines", "detail", id] as const, runs: (id: string) => ["routines", "runs", id] as const, revisions: (id: string) => ["routines", "revisions", id] as const, - activity: (companyId: string, id: string) => ["routines", "activity", companyId, id] as const, - documentAnnotations: (routineId: string, key: "description", status: "open" | "resolved" | "all" = "all") => - ["routines", "document-annotations", routineId, key, status] as const, + activity: (companyId: string, id: string) => + ["routines", "activity", companyId, id] as const, + documentAnnotations: ( + routineId: string, + key: "description", + status: "open" | "resolved" | "all" = "all", + ) => ["routines", "document-annotations", routineId, key, status] as const, }, folders: { - list: (companyId: string, kind: string) => ["folders", companyId, kind] as const, + list: (companyId: string, kind: string) => + ["folders", companyId, kind] as const, }, pipelines: { list: (companyId: string) => ["pipelines", companyId] as const, - detail: (pipelineId: string) => ["pipelines", "detail", pipelineId] as const, + detail: (pipelineId: string) => + ["pipelines", "detail", pipelineId] as const, cases: (pipelineId: string) => ["pipelines", "cases", pipelineId] as const, caseDetail: (caseId: string) => ["pipelines", "item", caseId] as const, - caseChildren: (caseId: string) => ["pipelines", "item", caseId, "children"] as const, - caseEvents: (caseId: string) => ["pipelines", "item", caseId, "events"] as const, - caseIssueLinks: (caseId: string) => ["pipelines", "item", caseId, "issue-links"] as const, - caseOutputs: (caseId: string) => ["pipelines", "item", caseId, "outputs"] as const, - caseDocument: (caseId: string, key: string) => ["pipelines", "item", caseId, "document", key] as const, + caseChildren: (caseId: string) => + ["pipelines", "item", caseId, "children"] as const, + caseEvents: (caseId: string) => + ["pipelines", "item", caseId, "events"] as const, + caseIssueLinks: (caseId: string) => + ["pipelines", "item", caseId, "issue-links"] as const, + caseOutputs: (caseId: string) => + ["pipelines", "item", caseId, "outputs"] as const, + caseDocument: (caseId: string, key: string) => + ["pipelines", "item", caseId, "document", key] as const, caseDocumentRevisions: (caseId: string, key: string) => ["pipelines", "item", caseId, "document-revisions", key] as const, - intakeForm: (pipelineId: string) => ["pipelines", "intake-form", pipelineId] as const, - health: (pipelineId: string) => ["pipelines", "health", pipelineId] as const, - document: (pipelineId: string, key: string) => ["pipelines", "document", pipelineId, key] as const, + intakeForm: (pipelineId: string) => + ["pipelines", "intake-form", pipelineId] as const, + health: (pipelineId: string) => + ["pipelines", "health", pipelineId] as const, + document: (pipelineId: string, key: string) => + ["pipelines", "document", pipelineId, key] as const, documentRevisions: (pipelineId: string, key: string) => ["pipelines", "document-revisions", pipelineId, key] as const, - attention: (companyId: string) => ["pipelines", "attention", companyId] as const, - reviewCases: (companyId: string) => ["pipelines", "review-cases", companyId] as const, - learnings: (companyId: string, offset: number) => ["pipelines", "learnings", companyId, offset] as const, + attention: (companyId: string) => + ["pipelines", "attention", companyId] as const, + reviewCases: (companyId: string) => + ["pipelines", "review-cases", companyId] as const, + learnings: (companyId: string, offset: number) => + ["pipelines", "learnings", companyId, offset] as const, }, executionWorkspaces: { - list: (companyId: string, filters?: Record) => - ["execution-workspaces", companyId, filters ?? {}] as const, - summaryList: (companyId: string, filters?: Record) => - ["execution-workspaces", companyId, "summary", filters ?? {}] as const, - overview: (companyId: string, filters?: Record) => + list: ( + companyId: string, + filters?: Record, + ) => ["execution-workspaces", companyId, filters ?? {}] as const, + summaryList: ( + companyId: string, + filters?: Record, + ) => ["execution-workspaces", companyId, "summary", filters ?? {}] as const, + overview: ( + companyId: string, + filters?: Record, + ) => ["execution-workspaces", companyId, "overview", filters ?? {}] as const, detail: (id: string) => ["execution-workspaces", "detail", id] as const, - closeReadiness: (id: string) => ["execution-workspaces", "close-readiness", id] as const, - workspaceOperations: (id: string) => ["execution-workspaces", "workspace-operations", id] as const, + closeReadiness: (id: string) => + ["execution-workspaces", "close-readiness", id] as const, + workspaceOperations: (id: string) => + ["execution-workspaces", "workspace-operations", id] as const, }, environments: { list: (companyId: string) => ["environments", companyId] as const, - capabilities: (companyId: string) => ["environment-capabilities", companyId] as const, + capabilities: (companyId: string) => + ["environment-capabilities", companyId] as const, customImageTemplate: (environmentId: string) => ["environments", environmentId, "custom-image-template"] as const, customImageSetupSession: (sessionId: string) => @@ -306,26 +482,37 @@ export const queryKeys = { projects: { all: (companyId: string) => ["projects", companyId] as const, list: (companyId: string, opts: { includeArchived?: boolean } = {}) => - ["projects", companyId, { includeArchived: opts.includeArchived === true }] as const, + [ + "projects", + companyId, + { includeArchived: opts.includeArchived === true }, + ] as const, detail: (id: string) => ["projects", "detail", id] as const, }, cases: { list: (companyId: string) => ["cases", companyId] as const, detail: (id: string) => ["cases", "detail", id] as const, documents: (id: string) => ["cases", "documents", id] as const, - documentAnnotations: (caseId: string, key: string, status: "open" | "resolved" | "all" = "all") => - ["cases", "document-annotations", caseId, key, status] as const, + documentAnnotations: ( + caseId: string, + key: string, + status: "open" | "resolved" | "all" = "all", + ) => ["cases", "document-annotations", caseId, key, status] as const, events: (id: string) => ["cases", "events", id] as const, children: (parentId: string) => ["cases", "children", parentId] as const, - revisions: (id: string, key: string) => ["cases", "revisions", id, key] as const, + revisions: (id: string, key: string) => + ["cases", "revisions", id, key] as const, forIssue: (issueId: string) => ["cases", "for-issue", issueId] as const, }, externalObjects: { - byIssue: (issueId: string) => ["external-objects", "by-issue", issueId] as const, - issueSummary: (issueId: string) => ["external-objects", "issue-summary", issueId] as const, + byIssue: (issueId: string) => + ["external-objects", "by-issue", issueId] as const, + issueSummary: (issueId: string) => + ["external-objects", "issue-summary", issueId] as const, issueSummaries: (companyId: string, issueIds: readonly string[]) => ["external-objects", "issue-summaries", companyId, issueIds] as const, - projectSummary: (projectId: string) => ["external-objects", "project-summary", projectId] as const, + projectSummary: (projectId: string) => + ["external-objects", "project-summary", projectId] as const, }, goals: { list: (companyId: string) => ["goals", companyId] as const, @@ -349,24 +536,31 @@ export const queryKeys = { ] as const, }, budgets: { - overview: (companyId: string) => ["budgets", "overview", companyId] as const, + overview: (companyId: string) => + ["budgets", "overview", companyId] as const, }, approvals: { list: (companyId: string, status?: string) => ["approvals", companyId, status] as const, - detail: (approvalId: string) => ["approvals", "detail", approvalId] as const, - comments: (approvalId: string) => ["approvals", "comments", approvalId] as const, - issues: (approvalId: string) => ["approvals", "issues", approvalId] as const, + detail: (approvalId: string) => + ["approvals", "detail", approvalId] as const, + comments: (approvalId: string) => + ["approvals", "comments", approvalId] as const, + issues: (approvalId: string) => + ["approvals", "issues", approvalId] as const, }, access: { invites: (companyId: string, state: string = "all", limit: number = 20) => ["access", "invites", "paginated-v1", companyId, state, limit] as const, joinRequests: (companyId: string, status: string = "pending_approval") => ["access", "join-requests", companyId, status] as const, - companyMembers: (companyId: string) => ["access", "company-members", companyId] as const, - companyUserDirectory: (companyId: string) => ["access", "company-user-directory", companyId] as const, + companyMembers: (companyId: string) => + ["access", "company-members", companyId] as const, + companyUserDirectory: (companyId: string) => + ["access", "company-user-directory", companyId] as const, adminUsers: (query: string) => ["access", "admin-users", query] as const, - userCompanyAccess: (userId: string) => ["access", "user-company-access", userId] as const, + userCompanyAccess: (userId: string) => + ["access", "user-company-access", userId] as const, invite: (token: string) => ["access", "invite", token] as const, currentBoardAccess: ["access", "current-board-access"] as const, }, @@ -374,15 +568,18 @@ export const queryKeys = { session: ["auth", "session"] as const, }, inboxAgentPolicy: { - mine: (companyId: string) => ["inbox-agent-policy", companyId, "me"] as const, + mine: (companyId: string) => + ["inbox-agent-policy", companyId, "me"] as const, }, sidebarPreferences: { - companyOrder: (userId: string) => ["sidebar-preferences", "company-order", userId] as const, + companyOrder: (userId: string) => + ["sidebar-preferences", "company-order", userId] as const, projectOrder: (companyId: string, userId: string) => ["sidebar-preferences", "project-order", companyId, userId] as const, }, resourceMemberships: { - mine: (companyId: string) => ["resource-memberships", companyId, "me"] as const, + mine: (companyId: string) => + ["resource-memberships", companyId, "me"] as const, }, instance: { settings: ["instance", "settings"] as const, @@ -397,19 +594,28 @@ export const queryKeys = { secrets: { list: (companyId: string) => ["secrets", companyId] as const, providers: (companyId: string) => ["secret-providers", companyId] as const, - providerConfigs: (companyId: string) => ["secret-provider-configs", companyId] as const, + providerConfigs: (companyId: string) => + ["secret-provider-configs", companyId] as const, usage: (secretId: string) => ["secrets", "usage", secretId] as const, - accessEvents: (secretId: string) => ["secrets", "access-events", secretId] as const, - userDefinitions: (companyId: string) => ["user-secret-definitions", companyId] as const, + accessEvents: (secretId: string) => + ["secrets", "access-events", secretId] as const, + userDefinitions: (companyId: string) => + ["user-secret-definitions", companyId] as const, userDefinitionCoverage: (companyId: string, definitionId: string) => ["user-secret-definitions", companyId, definitionId, "coverage"] as const, - myUserSecrets: (companyId: string) => ["my-user-secrets", companyId] as const, + myUserSecrets: (companyId: string) => + ["my-user-secrets", companyId] as const, proposals: (companyId: string, status: string = "pending") => ["secret-proposals", companyId, status] as const, }, companySearch: { - search: (companyId: string, q: string, scope: string, limit: number, offset: number) => - ["company-search", companyId, q, scope, limit, offset] as const, + search: ( + companyId: string, + q: string, + scope: string, + limit: number, + offset: number, + ) => ["company-search", companyId, q, scope, limit, offset] as const, }, dashboard: (companyId: string) => ["dashboard", companyId] as const, attention: (companyId: string) => ["attention", companyId] as const, @@ -422,16 +628,20 @@ export const queryKeys = { }, decisionQueues: { list: (companyId: string) => ["decision-queues", companyId] as const, - items: (companyId: string, key: string) => ["decision-queues", companyId, "items", key] as const, - seedRules: (companyId: string) => ["decision-queues", companyId, "seed-rules"] as const, + items: (companyId: string, key: string) => + ["decision-queues", companyId, "items", key] as const, + seedRules: (companyId: string) => + ["decision-queues", companyId, "seed-rules"] as const, triage: (companyId: string, sourceKind: string, sourceId: string) => ["decision-triage", companyId, sourceKind, sourceId] as const, }, - workTimeline: (companyId: string, lens?: string) => ["work-timeline", companyId, lens ?? "all"] as const, + workTimeline: (companyId: string, lens?: string) => + ["work-timeline", companyId, lens ?? "all"] as const, userProfile: (companyId: string, userSlug: string) => ["user-profile", companyId, userSlug] as const, sidebarBadges: (companyId: string) => ["sidebar-badges", companyId] as const, - inboxDismissals: (companyId: string) => ["inbox-dismissals", companyId] as const, + inboxDismissals: (companyId: string) => + ["inbox-dismissals", companyId] as const, activity: (companyId: string) => ["activity", companyId] as const, costs: (companyId: string, from?: string, to?: string) => ["costs", companyId, from, to] as const, @@ -445,8 +655,12 @@ export const queryKeys = { ["finance-by-biller", companyId, from, to] as const, financeByKind: (companyId: string, from?: string, to?: string) => ["finance-by-kind", companyId, from, to] as const, - financeEvents: (companyId: string, from?: string, to?: string, limit: number = 100) => - ["finance-events", companyId, from, to, limit] as const, + financeEvents: ( + companyId: string, + from?: string, + to?: string, + limit: number = 100, + ) => ["finance-events", companyId, from, to, limit] as const, usageWindowSpend: (companyId: string) => ["usage-window-spend", companyId] as const, usageQuotaWindows: (companyId: string) => @@ -454,7 +668,10 @@ export const queryKeys = { heartbeats: (companyId: string, agentId?: string) => ["heartbeats", companyId, agentId] as const, runDetail: (runId: string) => ["heartbeat-run", runId] as const, - runWorkspaceOperations: (runId: string) => ["heartbeat-run", runId, "workspace-operations"] as const, + providerTraceMetadata: (companyId: string, runIds: readonly string[]) => + ["provider-trace-metadata", companyId, [...runIds].sort()] as const, + runWorkspaceOperations: (runId: string) => + ["heartbeat-run", runId, "workspace-operations"] as const, liveRuns: (companyId: string) => ["live-runs", companyId] as const, runIssues: (runId: string) => ["run-issues", runId] as const, org: (companyId: string) => ["org", companyId] as const, @@ -467,10 +684,12 @@ export const queryKeys = { detail: (pluginId: string) => ["plugins", pluginId] as const, health: (pluginId: string) => ["plugins", pluginId, "health"] as const, uiContributions: ["plugins", "ui-contributions"] as const, - config: (pluginId: string, companyId: string) => ["plugins", pluginId, "companies", companyId, "config"] as const, + config: (pluginId: string, companyId: string) => + ["plugins", pluginId, "companies", companyId, "config"] as const, localFolders: (pluginId: string, companyId: string) => ["plugins", pluginId, "companies", companyId, "local-folders"] as const, - dashboard: (pluginId: string) => ["plugins", pluginId, "dashboard"] as const, + dashboard: (pluginId: string) => + ["plugins", pluginId, "dashboard"] as const, logs: (pluginId: string) => ["plugins", pluginId, "logs"] as const, }, adapters: { diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 1f247223b48..c2183041068 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -1,19 +1,40 @@ import { useCallback, useEffect, useMemo, useState, useRef } from "react"; -import { useParams, useNavigate, Link, Navigate, useBeforeUnload, type NavigateFunction } from "@/lib/router"; -import { useQuery, useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query"; +import { + useParams, + useNavigate, + Link, + Navigate, + useBeforeUnload, + type NavigateFunction, +} from "@/lib/router"; +import { + useQuery, + useMutation, + useQueryClient, + type QueryClient, +} from "@tanstack/react-query"; import { agentsApi, type AgentKey, type ClaudeLoginResult, type AgentPermissionUpdate, } from "../api/agents"; -import { builtInAgentsApi, type BuiltInManagedResourceKind } from "../api/builtInAgents"; +import { + builtInAgentsApi, + type BuiltInManagedResourceKind, +} from "../api/builtInAgents"; import { companySkillsApi } from "../api/companySkills"; import { budgetsApi } from "../api/budgets"; import { heartbeatsApi } from "../api/heartbeats"; import { instanceSettingsApi } from "../api/instanceSettings"; import { ApiError } from "../api/client"; -import { ChartCard, RunActivityChart, PriorityChart, IssueStatusChart, SuccessRateChart } from "../components/ActivityCharts"; +import { + ChartCard, + RunActivityChart, + PriorityChart, + IssueStatusChart, + SuccessRateChart, +} from "../components/ActivityCharts"; import { SHOW_TASK_PRIORITY_UI } from "../lib/ui-flags"; import { activityApi } from "../api/activity"; import { accessApi } from "../api/access"; @@ -29,7 +50,11 @@ import { copyTextToClipboard } from "../lib/clipboard"; import { AgentSkillsTab } from "./agent-skills/AgentSkillsTab"; import { AgentConfigForm } from "../components/AgentConfigForm"; import { PageTabBar } from "../components/PageTabBar"; -import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives"; +import { + adapterLabels, + roleLabels, + help, +} from "../components/agent-config-primitives"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { useAdapterCapabilities } from "@/adapters/use-adapter-capabilities"; import { redactCommandText as redactCommandSecretText } from "@paperclipai/adapter-utils"; @@ -59,13 +84,23 @@ import { SourceResolvedFoldCallout } from "../components/SourceResolvedFoldCallo import { SourceResolvedFoldBadge } from "../components/SourceResolvedFoldBadge"; import { readSourceResolvedWatchdogFold } from "../lib/source-resolved-watchdog-fold"; import { buildSameOriginWebSocketUrl } from "../lib/websocket-url"; -import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils"; +import { + formatCents, + formatDate, + relativeTime, + formatTokens, + visibleRunCostUsd, +} from "../lib/utils"; import { cn } from "../lib/utils"; import { describeRunRetryState } from "../lib/runRetryState"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs } from "@/components/ui/tabs"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { CheckCircle2, XCircle, @@ -86,11 +121,18 @@ import { FolderOpen, AlertTriangle, } from "lucide-react"; -import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; +import { + Collapsible, + CollapsibleTrigger, + CollapsibleContent, +} from "@/components/ui/collapsible"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Input } from "@/components/ui/input"; import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker"; -import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView"; +import { + RunTranscriptView, + type TranscriptMode, +} from "../components/transcript/RunTranscriptView"; import { AgentToolsTab } from "./AgentToolsTab"; import { appendCapped, @@ -113,8 +155,19 @@ import { } from "@paperclipai/shared"; import { ResponsibleUserDenialNotice } from "../components/ResponsibleUserDenialNotice"; import { RunWorkspaceRecoverySurface } from "../components/RunWorkspaceRecoverySurface"; -import { buildPermissionsForTrustPreset, getTrustPreset } from "../lib/trust-policy-ui"; -import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; +import { RunnerInspector } from "../components/RunnerInspector"; +import { + ProviderTraceStatusBadge, + runRequestedProviderTrace, +} from "../components/ProviderTraceStatusBadge"; +import { + buildPermissionsForTrustPreset, + getTrustPreset, +} from "../lib/trust-policy-ui"; +import { + redactHomePathUserSegments, + redactHomePathUserSegmentsInValue, +} from "@paperclipai/adapter-utils"; import { agentRouteRef } from "../lib/utils"; import { isStarred, @@ -124,8 +177,14 @@ import { } from "../hooks/useResourceMemberships"; import { Badge } from "@/components/ui/badge"; -const runStatusIcons: Record = { - succeeded: { icon: CheckCircle2, color: "text-green-600 dark:text-green-400" }, +const runStatusIcons: Record< + string, + { icon: typeof CheckCircle2; color: string } +> = { + succeeded: { + icon: CheckCircle2, + color: "text-green-600 dark:text-green-400", + }, failed: { icon: XCircle, color: "text-red-600 dark:text-red-400" }, running: { icon: Loader2, color: "text-blue-600 dark:text-blue-400" }, // Gallery feedback r1: running = status blue, not cyan. queued: { icon: Clock, color: "text-yellow-600 dark:text-yellow-400" }, @@ -139,13 +198,20 @@ const RUN_LOG_PAGE_BYTES = 256_000; const REDACTED_ENV_VALUE = "***REDACTED***"; const SECRET_ENV_KEY_RE = /(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i; -const COMMAND_ENV_KEY_RE = /(^command$|^cmd$|command[-_]?line|resolved[-_]?command|PAPERCLIP_RESOLVED_COMMAND)/i; -const JWT_VALUE_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?$/; +const COMMAND_ENV_KEY_RE = + /(^command$|^cmd$|command[-_]?line|resolved[-_]?command|PAPERCLIP_RESOLVED_COMMAND)/i; +const JWT_VALUE_RE = + /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?$/; function formatOrgChainHealthPath(agent: AgentDetailRecord) { - return agent.orgChainHealth?.fullChain - .map((entry) => `${entry.name}${entry.status !== "active" && entry.status !== "idle" ? ` (${entry.status})` : ""}`) - .join(" -> ") ?? agent.name; + return ( + agent.orgChainHealth?.fullChain + .map( + (entry) => + `${entry.name}${entry.status !== "active" && entry.status !== "idle" ? ` (${entry.status})` : ""}`, + ) + .join(" -> ") ?? agent.name + ); } function redactPathText(value: string, censorUsernameInLogs: boolean) { @@ -153,11 +219,19 @@ function redactPathText(value: string, censorUsernameInLogs: boolean) { } function redactPathValue(value: T, censorUsernameInLogs: boolean): T { - return redactHomePathUserSegmentsInValue(value, { enabled: censorUsernameInLogs }); + return redactHomePathUserSegmentsInValue(value, { + enabled: censorUsernameInLogs, + }); } -function redactCommandText(value: string, censorUsernameInLogs: boolean): string { - return redactPathText(redactCommandSecretText(value, REDACTED_ENV_VALUE), censorUsernameInLogs); +function redactCommandText( + value: string, + censorUsernameInLogs: boolean, +): string { + return redactPathText( + redactCommandSecretText(value, REDACTED_ENV_VALUE), + censorUsernameInLogs, + ); } function shouldRedactSecretValue(key: string, value: unknown): boolean { @@ -166,7 +240,11 @@ function shouldRedactSecretValue(key: string, value: unknown): boolean { return JWT_VALUE_RE.test(value); } -function redactEnvValue(key: string, value: unknown, censorUsernameInLogs: boolean): string { +function redactEnvValue( + key: string, + value: unknown, + censorUsernameInLogs: boolean, +): string { if ( typeof value === "object" && value !== null && @@ -177,8 +255,10 @@ function redactEnvValue(key: string, value: unknown, censorUsernameInLogs: boole } if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE; if (value === null || value === undefined) return ""; - if (typeof value === "string" && COMMAND_ENV_KEY_RE.test(key)) return redactCommandText(value, censorUsernameInLogs); - if (typeof value === "string") return redactPathText(value, censorUsernameInLogs); + if (typeof value === "string" && COMMAND_ENV_KEY_RE.test(key)) + return redactCommandText(value, censorUsernameInLogs); + if (typeof value === "string") + return redactPathText(value, censorUsernameInLogs); try { return JSON.stringify(redactPathValue(value, censorUsernameInLogs)); } catch { @@ -201,7 +281,10 @@ function shouldUseMarkdownInstructionsEditor(input: { return isMarkdown(input.selectedPath); } -function formatEnvForDisplay(envValue: unknown, censorUsernameInLogs: boolean): string { +function formatEnvForDisplay( + envValue: unknown, + censorUsernameInLogs: boolean, +): string { const env = asRecord(envValue); if (!env) return ""; @@ -210,7 +293,9 @@ function formatEnvForDisplay(envValue: unknown, censorUsernameInLogs: boolean): return keys .sort() - .map((key) => `${key}=${redactEnvValue(key, env[key], censorUsernameInLogs)}`) + .map( + (key) => `${key}=${redactEnvValue(key, env[key], censorUsernameInLogs)}`, + ) .join("\n"); } @@ -230,7 +315,9 @@ function isWindowContainer(container: ScrollContainer): container is Window { function isElementScrollContainer(element: HTMLElement): boolean { const overflowY = window.getComputedStyle(element).overflowY; - return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay"; + return ( + overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay" + ); } function findScrollContainer(anchor: HTMLElement | null): ScrollContainer { @@ -242,7 +329,10 @@ function findScrollContainer(anchor: HTMLElement | null): ScrollContainer { return window; } -function readScrollMetrics(container: ScrollContainer): { scrollHeight: number; distanceFromBottom: number } { +function readScrollMetrics(container: ScrollContainer): { + scrollHeight: number; + distanceFromBottom: number; +} { if (isWindowContainer(container)) { const pageHeight = Math.max( document.documentElement.scrollHeight, @@ -262,7 +352,10 @@ function readScrollMetrics(container: ScrollContainer): { scrollHeight: number; }; } -function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBehavior = "auto") { +function scrollToContainerBottom( + container: ScrollContainer, + behavior: ScrollBehavior = "auto", +) { if (isWindowContainer(container)) { const pageHeight = Math.max( document.documentElement.scrollHeight, @@ -275,9 +368,21 @@ function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBeh container.scrollTo({ top: container.scrollHeight, behavior }); } -type AgentDetailView = "dashboard" | "instructions" | "configuration" | "secrets" | "skills" | "tools" | "runs" | "audit" | "budget"; +type AgentDetailView = + | "dashboard" + | "instructions" + | "configuration" + | "secrets" + | "skills" + | "tools" + | "runs" + | "audit" + | "budget"; -export const AGENT_DETAIL_TABS: ReadonlyArray<{ value: AgentDetailView; label: string }> = [ +export const AGENT_DETAIL_TABS: ReadonlyArray<{ + value: AgentDetailView; + label: string; +}> = [ { value: "dashboard", label: "Dashboard" }, { value: "instructions", label: "Instructions" }, { value: "skills", label: "Skills" }, @@ -289,7 +394,8 @@ export const AGENT_DETAIL_TABS: ReadonlyArray<{ value: AgentDetailView; label: s { value: "budget", label: "Budget" }, ]; -export const DISCARD_AGENT_CONFIG_CHANGES_MESSAGE = "Discard unsaved agent configuration changes?"; +export const DISCARD_AGENT_CONFIG_CHANGES_MESSAGE = + "Discard unsaved agent configuration changes?"; export function confirmAgentConfigNavigation( dirty: boolean, @@ -299,8 +405,12 @@ export function confirmAgentConfigNavigation( return !dirty || confirm(DISCARD_AGENT_CONFIG_CHANGES_MESSAGE); } -export function agentConfigHistoryRestoreDelta(currentIndex: unknown, nextIndex: unknown): number | null { - if (typeof currentIndex !== "number" || typeof nextIndex !== "number") return null; +export function agentConfigHistoryRestoreDelta( + currentIndex: unknown, + nextIndex: unknown, +): number | null { + if (typeof currentIndex !== "number" || typeof nextIndex !== "number") + return null; const delta = currentIndex - nextIndex; return delta === 0 ? null : delta; } @@ -310,7 +420,10 @@ export function restoreAgentConfigHistoryEntry( currentEntry: { index: unknown; state: unknown; url: string }, nextIndex: unknown, ): boolean { - const restoreDelta = agentConfigHistoryRestoreDelta(currentEntry.index, nextIndex); + const restoreDelta = agentConfigHistoryRestoreDelta( + currentEntry.index, + nextIndex, + ); if (restoreDelta === null) { // Some legacy URL-cleanup paths erased React Router's history index. A // fresh copy of the guarded entry is the only safe way to return without @@ -325,7 +438,8 @@ export function restoreAgentConfigHistoryEntry( export function parseAgentDetailView(value: string | null): AgentDetailView { if (value === "instructions" || value === "prompts") return "instructions"; - if (value === "configure" || value === "configuration") return "configuration"; + if (value === "configure" || value === "configuration") + return "configuration"; if (value === "secrets") return "secrets"; if (value === "skills") return "skills"; if (value === "tools") return "tools"; @@ -363,8 +477,7 @@ function runMetrics(run: HeartbeatRun) { "cached_input_tokens", "cache_read_input_tokens", ); - const cost = - visibleRunCostUsd(usage, result); + const cost = visibleRunCostUsd(usage, result); const provider = asNonEmptyString(usage?.provider) ?? null; const model = asNonEmptyString(usage?.model) ?? null; return { @@ -385,7 +498,8 @@ export type RunLogChunk = { }; function asRecord(value: unknown): Record | null { - if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + if (typeof value !== "object" || value === null || Array.isArray(value)) + return null; return value as Record; } @@ -415,7 +529,9 @@ export function shouldPollRunShellLog(status: HeartbeatRun["status"]): boolean { return status === "running"; } -export function runDetailRefetchIntervalMs(status: HeartbeatRun["status"]): 5000 | 15000 | false { +export function runDetailRefetchIntervalMs( + status: HeartbeatRun["status"], +): 5000 | 15000 | false { if (status === "queued") return 5000; if (status === "running") return 15000; return false; @@ -431,28 +547,40 @@ export function RunInvocationCard({ const rawCommandLine = [ typeof payload.command === "string" ? payload.command : null, ...(Array.isArray(payload.commandArgs) - ? payload.commandArgs.filter((value): value is string => typeof value === "string") + ? payload.commandArgs.filter( + (value): value is string => typeof value === "string", + ) : []), ] .filter((value): value is string => Boolean(value)) .join(" "); - const commandLine = rawCommandLine ? redactCommandText(rawCommandLine, censorUsernameInLogs) : ""; + const commandLine = rawCommandLine + ? redactCommandText(rawCommandLine, censorUsernameInLogs) + : ""; const hasAdvancedDetails = - commandLine.length > 0 - || (Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0) - || payload.prompt !== undefined - || payload.context !== undefined - || payload.env !== undefined; + commandLine.length > 0 || + (Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0) || + payload.prompt !== undefined || + payload.context !== undefined || + payload.env !== undefined; return (
-
Invocation
+
+ Invocation +
{typeof payload.adapterType === "string" && ( -
Adapter: {payload.adapterType}
+
+ Adapter: + {payload.adapterType} +
)} {typeof payload.cwd === "string" && ( -
Working dir: {payload.cwd}
+
+ Working dir: + {payload.cwd} +
)} {hasAdvancedDetails && ( @@ -467,41 +595,62 @@ export function RunInvocationCard({ {commandLine}
)} - {Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0 && ( -
-
Command notes
-
    - {payload.commandNotes - .filter((value): value is string => typeof value === "string" && value.trim().length > 0) - .map((note, idx) => ( -
  • - {note} -
  • - ))} -
-
- )} + {Array.isArray(payload.commandNotes) && + payload.commandNotes.length > 0 && ( +
+
+ Command notes +
+
    + {payload.commandNotes + .filter( + (value): value is string => + typeof value === "string" && value.trim().length > 0, + ) + .map((note, idx) => ( +
  • + {note} +
  • + ))} +
+
+ )} {payload.prompt !== undefined && (
Prompt
                   {typeof payload.prompt === "string"
                     ? redactPathText(payload.prompt, censorUsernameInLogs)
-                    : JSON.stringify(redactPathValue(payload.prompt, censorUsernameInLogs), null, 2)}
+                    : JSON.stringify(
+                        redactPathValue(payload.prompt, censorUsernameInLogs),
+                        null,
+                        2,
+                      )}
                 
)} {payload.context !== undefined && (
-
Context
+
+ Context +
-                  {JSON.stringify(redactPathValue(payload.context, censorUsernameInLogs), null, 2)}
+                  {JSON.stringify(
+                    redactPathValue(payload.context, censorUsernameInLogs),
+                    null,
+                    2,
+                  )}
                 
)} {payload.env !== undefined && (
-
Environment
+
+ Environment +
                   {formatEnvForDisplay(payload.env, censorUsernameInLogs)}
                 
@@ -520,9 +669,15 @@ function parseStoredLogContent(content: string): RunLogChunk[] { const trimmed = line.trim(); if (!trimmed) continue; try { - const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown }; + const raw = JSON.parse(trimmed) as { + ts?: unknown; + stream?: unknown; + chunk?: unknown; + }; const stream = - raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; + raw.stream === "stderr" || raw.stream === "system" + ? raw.stream + : "stdout"; const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); if (!chunk) continue; @@ -566,9 +721,14 @@ function workspaceOperationStatusTone(status: WorkspaceOperation["status"]) { } } -function WorkspaceOperationStatusBadge({ status }: { status: WorkspaceOperation["status"] }) { +function WorkspaceOperationStatusBadge({ + status, +}: { + status: WorkspaceOperation["status"]; +}) { return ( - heartbeatsApi.workspaceOperationLog(operation.id), enabled: open && Boolean(operation.logRef), @@ -610,21 +774,29 @@ function WorkspaceOperationLogViewer({ {open && (
- {isLoading &&
Loading log...
} + {isLoading && ( +
Loading log...
+ )} {error && (
- {error instanceof Error ? error.message : "Failed to load workspace operation log"} + {error instanceof Error + ? error.message + : "Failed to load workspace operation log"}
)} {!isLoading && !error && chunks.length === 0 && ( -
No persisted log lines.
+
+ No persisted log lines. +
)} {chunks.length > 0 && (
{chunks.map((chunk, index) => (
- {new Date(chunk.ts).toLocaleTimeString("en-US", { hour12: false })} + {new Date(chunk.ts).toLocaleTimeString("en-US", { + hour12: false, + })} [{chunk.stream}] - {redactPathText(chunk.chunk, censorUsernameInLogs)} + + {redactPathText(chunk.chunk, censorUsernameInLogs)} +
))}
@@ -667,13 +841,19 @@ function WorkspaceOperationsSection({ {operations.map((operation) => { const metadata = asRecord(operation.metadata); return ( -
+
-
{workspaceOperationPhaseLabel(operation.phase)}
+
+ {workspaceOperationPhaseLabel(operation.phase)} +
{relativeTime(operation.startedAt)} - {operation.finishedAt && ` to ${relativeTime(operation.finishedAt)}`} + {operation.finishedAt && + ` to ${relativeTime(operation.finishedAt)}`}
{operation.command && ( @@ -688,47 +868,84 @@ function WorkspaceOperationsSection({ {operation.cwd}
)} - {(asNonEmptyString(metadata?.branchName) - || asNonEmptyString(metadata?.baseRef) - || asNonEmptyString(metadata?.worktreePath) - || asNonEmptyString(metadata?.repoRoot) - || asNonEmptyString(metadata?.cleanupAction)) && ( + {(asNonEmptyString(metadata?.branchName) || + asNonEmptyString(metadata?.baseRef) || + asNonEmptyString(metadata?.worktreePath) || + asNonEmptyString(metadata?.repoRoot) || + asNonEmptyString(metadata?.cleanupAction)) && (
{asNonEmptyString(metadata?.branchName) && ( -
Branch: {metadata?.branchName as string}
+
+ Branch: + + {metadata?.branchName as string} + +
)} {asNonEmptyString(metadata?.baseRef) && ( -
Base ref: {metadata?.baseRef as string}
+
+ Base ref: + + {metadata?.baseRef as string} + +
)} {asNonEmptyString(metadata?.worktreePath) && ( -
Worktree: {metadata?.worktreePath as string}
+
+ Worktree: + + {metadata?.worktreePath as string} + +
)} {asNonEmptyString(metadata?.repoRoot) && ( -
Repo root: {metadata?.repoRoot as string}
+
+ Repo root: + + {metadata?.repoRoot as string} + +
)} {asNonEmptyString(metadata?.cleanupAction) && ( -
Cleanup: {metadata?.cleanupAction as string}
+
+ Cleanup: + + {metadata?.cleanupAction as string} + +
)}
)} {typeof metadata?.created === "boolean" && (
- {metadata.created ? "Created by this run" : "Reused existing workspace"} + {metadata.created + ? "Created by this run" + : "Reused existing workspace"}
)} {operation.stderrExcerpt && operation.stderrExcerpt.trim() && (
-
stderr excerpt
+
+ stderr excerpt +
-                    {redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
+                    {redactPathText(
+                      operation.stderrExcerpt,
+                      censorUsernameInLogs,
+                    )}
                   
)} {operation.stdoutExcerpt && operation.stdoutExcerpt.trim() && (
-
stdout excerpt
+
+ stdout excerpt +
-                    {redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
+                    {redactPathText(
+                      operation.stdoutExcerpt,
+                      censorUsernameInLogs,
+                    )}
                   
)} @@ -747,7 +964,12 @@ function WorkspaceOperationsSection({ } export function AgentDetail() { - const { companyPrefix, agentId, tab: urlTab, runId: urlRunId } = useParams<{ + const { + companyPrefix, + agentId, + tab: urlTab, + runId: urlRunId, + } = useParams<{ companyPrefix?: string; agentId: string; tab?: string; @@ -759,8 +981,12 @@ export function AgentDetail() { const queryClient = useQueryClient(); const navigate = useNavigate(); const [actionError, setActionError] = useState(null); - const [dismissedLeftAgentIds, setDismissedLeftAgentIds] = useState>(() => new Set()); - const activeView = urlRunId ? "runs" as AgentDetailView : parseAgentDetailView(urlTab ?? null); + const [dismissedLeftAgentIds, setDismissedLeftAgentIds] = useState< + Set + >(() => new Set()); + const activeView = urlRunId + ? ("runs" as AgentDetailView) + : parseAgentDetailView(urlTab ?? null); const needsDashboardData = activeView === "dashboard"; const needsRunData = activeView === "runs" || Boolean(urlRunId); const shouldLoadHeartbeats = needsDashboardData || needsRunData; @@ -773,18 +999,35 @@ export function AgentDetail() { const routeCompanyId = useMemo(() => { if (!companyPrefix) return null; const requestedPrefix = companyPrefix.toUpperCase(); - return companies.find((company) => company.issuePrefix.toUpperCase() === requestedPrefix)?.id ?? null; + return ( + companies.find( + (company) => company.issuePrefix.toUpperCase() === requestedPrefix, + )?.id ?? null + ); }, [companies, companyPrefix]); const lookupCompanyId = routeCompanyId ?? selectedCompanyId ?? undefined; - const canFetchAgent = routeAgentRef.length > 0 && (isUuidLike(routeAgentRef) || Boolean(lookupCompanyId)); - const setSaveConfigAction = useCallback((fn: (() => void) | null) => { saveConfigActionRef.current = fn; }, []); - const setCancelConfigAction = useCallback((fn: (() => void) | null) => { cancelConfigActionRef.current = fn; }, []); + const canFetchAgent = + routeAgentRef.length > 0 && + (isUuidLike(routeAgentRef) || Boolean(lookupCompanyId)); + const setSaveConfigAction = useCallback((fn: (() => void) | null) => { + saveConfigActionRef.current = fn; + }, []); + const setCancelConfigAction = useCallback((fn: (() => void) | null) => { + cancelConfigActionRef.current = fn; + }, []); const prepareAgentNavigation = useCallback(() => { return confirmAgentConfigNavigation(configDirty); }, [configDirty]); - const { data: agent, isLoading, error } = useQuery({ - queryKey: [...queryKeys.agents.detail(routeAgentRef), lookupCompanyId ?? null], + const { + data: agent, + isLoading, + error, + } = useQuery({ + queryKey: [ + ...queryKeys.agents.detail(routeAgentRef), + lookupCompanyId ?? null, + ], queryFn: () => agentsApi.get(routeAgentRef, lookupCompanyId), enabled: canFetchAgent, }); @@ -792,6 +1035,14 @@ export function AgentDetail() { const canonicalAgentRef = agent ? agentRouteRef(agent) : routeAgentRef; const agentLookupRef = agent?.id ?? routeAgentRef; const resolvedAgentId = agent?.id ?? null; + const { data: boardAccess } = useQuery({ + queryKey: queryKeys.access.currentBoardAccess, + queryFn: () => accessApi.getCurrentBoardAccess(), + retry: false, + }); + const canUseProviderTrace = + boardAccess?.source === "local_implicit" || + boardAccess?.isInstanceAdmin === true; const membershipsQuery = useResourceMemberships(resolvedCompanyId); const membershipMutation = useResourceMembershipMutation(resolvedCompanyId); const agentMembershipState = resolvedAgentId @@ -803,14 +1054,16 @@ export function AgentDetail() { queryFn: () => instanceSettingsApi.getExperimental(), enabled: !!resolvedCompanyId, }); - const builtInAgentsEnabled = experimentalSettings?.enableBuiltInAgents === true; + const builtInAgentsEnabled = + experimentalSettings?.enableBuiltInAgents === true; const { data: builtInStates } = useQuery({ queryKey: queryKeys.builtInAgents.list(resolvedCompanyId!), queryFn: () => builtInAgentsApi.list(resolvedCompanyId!), enabled: !!resolvedCompanyId && builtInAgentsEnabled, }); const builtInState = builtInAgentsEnabled - ? builtInStates?.find((entry) => entry.agentId === resolvedAgentId) ?? null + ? (builtInStates?.find((entry) => entry.agentId === resolvedAgentId) ?? + null) : null; const builtInFeatureLabel = builtInState ? builtInState.definition.featureKeys @@ -818,75 +1071,120 @@ export function AgentDetail() { .join(", ") : ""; const invalidateBuiltIn = useCallback(() => { - queryClient.invalidateQueries({ queryKey: queryKeys.builtInAgents.list(resolvedCompanyId!) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.builtInAgents.list(resolvedCompanyId!), + }); if (resolvedAgentId) { - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(resolvedAgentId) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(resolvedAgentId), + }); } - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(routeAgentRef), + }); }, [queryClient, resolvedCompanyId, resolvedAgentId, routeAgentRef]); const resetBuiltIn = useMutation({ - mutationFn: () => builtInAgentsApi.reset(resolvedCompanyId!, builtInState!.definition.key), + mutationFn: () => + builtInAgentsApi.reset(resolvedCompanyId!, builtInState!.definition.key), onSuccess: invalidateBuiltIn, }); const [showBuiltInConfigure, setShowBuiltInConfigure] = useState(false); const resetBuiltInResource = useMutation({ mutationFn: (kind: BuiltInManagedResourceKind) => - builtInAgentsApi.reset(resolvedCompanyId!, builtInState!.definition.key, [kind]), + builtInAgentsApi.reset(resolvedCompanyId!, builtInState!.definition.key, [ + kind, + ]), onSuccess: invalidateBuiltIn, onError: (error) => { - setActionError(error instanceof Error ? error.message : "Failed to update bundle resource"); + setActionError( + error instanceof Error + ? error.message + : "Failed to update bundle resource", + ); }, }); const runBuiltInRoutine = useMutation({ mutationFn: (routineKey: string) => - builtInAgentsApi.runRoutine(resolvedCompanyId!, builtInState!.definition.key, routineKey), + builtInAgentsApi.runRoutine( + resolvedCompanyId!, + builtInState!.definition.key, + routineKey, + ), onSuccess: invalidateBuiltIn, onError: (error) => { - setActionError(error instanceof Error ? error.message : "Failed to run built-in routine"); + setActionError( + error instanceof Error + ? error.message + : "Failed to run built-in routine", + ); }, }); const enableBuiltInSchedule = useMutation({ mutationFn: (routineKey: string) => - builtInAgentsApi.enableRoutineSchedule(resolvedCompanyId!, builtInState!.definition.key, routineKey), + builtInAgentsApi.enableRoutineSchedule( + resolvedCompanyId!, + builtInState!.definition.key, + routineKey, + ), onSuccess: invalidateBuiltIn, onError: (error) => { - setActionError(error instanceof Error ? error.message : "Failed to enable routine schedule"); + setActionError( + error instanceof Error + ? error.message + : "Failed to enable routine schedule", + ); }, }); const disableBuiltInSchedule = useMutation({ mutationFn: (routineKey: string) => - builtInAgentsApi.disableRoutineSchedule(resolvedCompanyId!, builtInState!.definition.key, routineKey), + builtInAgentsApi.disableRoutineSchedule( + resolvedCompanyId!, + builtInState!.definition.key, + routineKey, + ), onSuccess: invalidateBuiltIn, onError: (error) => { - setActionError(error instanceof Error ? error.message : "Failed to disable routine schedule"); + setActionError( + error instanceof Error + ? error.message + : "Failed to disable routine schedule", + ); }, }); - const builtInRoutineActionPending = - runBuiltInRoutine.isPending - ? "run" - : enableBuiltInSchedule.isPending - ? "enable" - : disableBuiltInSchedule.isPending - ? "disable" - : null; + const builtInRoutineActionPending = runBuiltInRoutine.isPending + ? "run" + : enableBuiltInSchedule.isPending + ? "enable" + : disableBuiltInSchedule.isPending + ? "disable" + : null; const { data: runtimeState } = useQuery({ queryKey: queryKeys.agents.runtimeState(resolvedAgentId ?? routeAgentRef), - queryFn: () => agentsApi.runtimeState(resolvedAgentId!, resolvedCompanyId ?? undefined), + queryFn: () => + agentsApi.runtimeState(resolvedAgentId!, resolvedCompanyId ?? undefined), enabled: Boolean(resolvedAgentId) && needsDashboardData, }); const { data: heartbeats } = useQuery({ queryKey: queryKeys.heartbeats(resolvedCompanyId!, agent?.id ?? undefined), - queryFn: () => heartbeatsApi.list(resolvedCompanyId!, agent?.id ?? undefined), + queryFn: () => + heartbeatsApi.list(resolvedCompanyId!, agent?.id ?? undefined), enabled: !!resolvedCompanyId && !!agent?.id && shouldLoadHeartbeats, }); const { data: allIssues } = useQuery({ - queryKey: [...queryKeys.issues.list(resolvedCompanyId!), "participant-agent", resolvedAgentId ?? "__none__"], - queryFn: () => issuesApi.list(resolvedCompanyId!, { participantAgentId: resolvedAgentId! }), + queryKey: [ + ...queryKeys.issues.list(resolvedCompanyId!), + "participant-agent", + resolvedAgentId ?? "__none__", + ], + queryFn: () => + issuesApi.list(resolvedCompanyId!, { + participantAgentId: resolvedAgentId!, + }), enabled: !!resolvedCompanyId && !!resolvedAgentId && needsDashboardData, }); @@ -904,13 +1202,20 @@ export function AgentDetail() { staleTime: 5_000, }); - const assignedIssues = (allIssues ?? []) - .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); - const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo); - const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agent?.id && a.status !== "terminated"); + const assignedIssues = (allIssues ?? []).sort( + (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), + ); + const reportsToAgent = (allAgents ?? []).find( + (a) => a.id === agent?.reportsTo, + ); + const directReports = (allAgents ?? []).filter( + (a) => a.reportsTo === agent?.id && a.status !== "terminated", + ); const agentBudgetSummary = useMemo(() => { const matched = budgetOverview?.policies.find( - (policy) => policy.scopeType === "agent" && policy.scopeId === (agent?.id ?? routeAgentRef), + (policy) => + policy.scopeType === "agent" && + policy.scopeId === (agent?.id ?? routeAgentRef), ); if (matched) return matched; const budgetMonthlyCents = agent?.budgetMonthlyCents ?? 0; @@ -927,12 +1232,17 @@ export function AgentDetail() { observedAmount: spentMonthlyCents, remainingAmount: Math.max(0, budgetMonthlyCents - spentMonthlyCents), utilizationPercent: - budgetMonthlyCents > 0 ? Number(((spentMonthlyCents / budgetMonthlyCents) * 100).toFixed(2)) : 0, + budgetMonthlyCents > 0 + ? Number(((spentMonthlyCents / budgetMonthlyCents) * 100).toFixed(2)) + : 0, warnPercent: 80, hardStopEnabled: true, notifyEnabled: true, isActive: budgetMonthlyCents > 0, - status: budgetMonthlyCents > 0 && spentMonthlyCents >= budgetMonthlyCents ? "hard_stop" : "ok", + status: + budgetMonthlyCents > 0 && spentMonthlyCents >= budgetMonthlyCents + ? "hard_stop" + : "ok", paused: agent?.status === "paused", pauseReason: agent?.pauseReason ?? null, windowStart: new Date(), @@ -940,7 +1250,10 @@ export function AgentDetail() { } satisfies BudgetPolicySummary; }, [agent, budgetOverview?.policies, resolvedCompanyId, routeAgentRef]); const mobileLiveRun = useMemo( - () => (heartbeats ?? []).find((r) => r.status === "running" || r.status === "queued") ?? null, + () => + (heartbeats ?? []).find( + (r) => r.status === "running" || r.status === "queued", + ) ?? null, [heartbeats], ); @@ -948,7 +1261,9 @@ export function AgentDetail() { if (!agent) return; if (urlRunId) { if (routeAgentRef !== canonicalAgentRef) { - navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true }); + navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { + replace: true, + }); } return; } @@ -971,10 +1286,20 @@ export function AgentDetail() { ? "budget" : "dashboard"; if (routeAgentRef !== canonicalAgentRef || urlTab !== canonicalTab) { - navigate(`/agents/${canonicalAgentRef}/${canonicalTab}`, { replace: true }); + navigate(`/agents/${canonicalAgentRef}/${canonicalTab}`, { + replace: true, + }); return; } - }, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, activeView, navigate]); + }, [ + agent, + routeAgentRef, + canonicalAgentRef, + urlRunId, + urlTab, + activeView, + navigate, + ]); useEffect(() => { if (!agent?.companyId || agent.companyId === selectedCompanyId) return; @@ -986,21 +1311,37 @@ export function AgentDetail() { // which is surfaced via the pending-approval banner below. const agentAction = useMutation({ mutationFn: async (action: "approve") => { - if (!agentLookupRef) return Promise.reject(new Error("No agent reference")); + if (!agentLookupRef) + return Promise.reject(new Error("No agent reference")); if (action === "approve") { - return agentsApi.approve(agentLookupRef, resolvedCompanyId ?? undefined); + return agentsApi.approve( + agentLookupRef, + resolvedCompanyId ?? undefined, + ); } }, onSuccess: () => { setActionError(null); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agentLookupRef) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agentLookupRef) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(routeAgentRef), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(agentLookupRef), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.runtimeState(agentLookupRef), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.taskSessions(agentLookupRef), + }); if (resolvedCompanyId) { - queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.list(resolvedCompanyId), + }); if (agent?.id) { - queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(resolvedCompanyId, agent.id) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.heartbeats(resolvedCompanyId, agent.id), + }); } } }, @@ -1019,38 +1360,71 @@ export function AgentDetail() { }), onSuccess: () => { if (!resolvedCompanyId) return; - queryClient.invalidateQueries({ queryKey: queryKeys.budgets.overview(resolvedCompanyId) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); - queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(resolvedCompanyId) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.budgets.overview(resolvedCompanyId), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(routeAgentRef), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(agentLookupRef), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.list(resolvedCompanyId), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.dashboard(resolvedCompanyId), + }); }, }); const updateIcon = useMutation({ - mutationFn: (icon: string) => agentsApi.update(agentLookupRef, { icon }, resolvedCompanyId ?? undefined), + mutationFn: (icon: string) => + agentsApi.update( + agentLookupRef, + { icon }, + resolvedCompanyId ?? undefined, + ), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(routeAgentRef), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(agentLookupRef), + }); if (resolvedCompanyId) { - queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.list(resolvedCompanyId), + }); } }, }); const updatePermissions = useMutation({ mutationFn: (permissions: AgentPermissionUpdate) => - agentsApi.updatePermissions(agentLookupRef, permissions, resolvedCompanyId ?? undefined), + agentsApi.updatePermissions( + agentLookupRef, + permissions, + resolvedCompanyId ?? undefined, + ), onSuccess: () => { setActionError(null); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); - queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(routeAgentRef), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.detail(agentLookupRef), + }); if (resolvedCompanyId) { - queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.list(resolvedCompanyId), + }); } }, onError: (err) => { - setActionError(err instanceof Error ? err.message : "Failed to update permissions"); + setActionError( + err instanceof Error ? err.message : "Failed to update permissions", + ); }, }); @@ -1062,9 +1436,15 @@ export function AgentDetail() { if (activeView === "dashboard" && !urlRunId) { crumbs.push({ label: agentName }); } else { - crumbs.push({ label: agentName, href: `/agents/${canonicalAgentRef}/dashboard` }); + crumbs.push({ + label: agentName, + href: `/agents/${canonicalAgentRef}/dashboard`, + }); if (urlRunId) { - crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` }); + crumbs.push({ + label: "Runs", + href: `/agents/${canonicalAgentRef}/runs`, + }); crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` }); } else if (activeView === "instructions") { crumbs.push({ label: "Instructions" }); @@ -1072,8 +1452,8 @@ export function AgentDetail() { crumbs.push({ label: "Configuration" }); } else if (activeView === "secrets") { crumbs.push({ label: "Secrets" }); - // } else if (activeView === "skills") { // TODO: bring back later - // crumbs.push({ label: "Skills" }); + // } else if (activeView === "skills") { // TODO: bring back later + // crumbs.push({ label: "Skills" }); } else if (activeView === "tools") { crumbs.push({ label: "Tools" }); } else if (activeView === "runs") { @@ -1085,7 +1465,14 @@ export function AgentDetail() { } } setBreadcrumbs(crumbs); - }, [setBreadcrumbs, agent, routeAgentRef, canonicalAgentRef, activeView, urlRunId]); + }, [ + setBreadcrumbs, + agent, + routeAgentRef, + canonicalAgentRef, + activeView, + urlRunId, + ]); useEffect(() => { closePanel(); @@ -1103,11 +1490,14 @@ export function AgentDetail() { }, [resolvedAgentId, agentMembershipState]); useBeforeUnload( - useCallback((event) => { - if (!configDirty) return; - event.preventDefault(); - event.returnValue = ""; - }, [configDirty]), + useCallback( + (event) => { + if (!configDirty) return; + event.preventDefault(); + event.returnValue = ""; + }, + [configDirty], + ), ); useEffect(() => { @@ -1146,7 +1536,8 @@ export function AgentDetail() { } document.addEventListener("click", handleDocumentClick, true); - return () => document.removeEventListener("click", handleDocumentClick, true); + return () => + document.removeEventListener("click", handleDocumentClick, true); }, [configDirty, prepareAgentNavigation]); useEffect(() => { @@ -1171,7 +1562,11 @@ export function AgentDetail() { if (prepareAgentNavigation()) return; event.stopImmediatePropagation(); - restoring = restoreAgentConfigHistoryEntry(window.history, currentEntry, event.state?.idx); + restoring = restoreAgentConfigHistoryEntry( + window.history, + currentEntry, + event.state?.idx, + ); } window.addEventListener("popstate", handlePopState, true); @@ -1185,19 +1580,29 @@ export function AgentDetail() { return ; } const isPendingApproval = agent.status === "pending_approval"; - const hasInvalidOrgChain = agent.orgChainHealth?.status === "invalid_org_chain"; - const pausedEscalationWarning = !hasInvalidOrgChain ? agent.orgChainHealth?.escalationWarning ?? null : null; - const showConfigActionBar = ( - activeView === "configuration" || activeView === "instructions" || activeView === "secrets" - ) && (configDirty || configSaving); - const showLeftAgentNotice = agentMembershipState === "left" && !dismissedLeftAgentIds.has(agent.id); + const hasInvalidOrgChain = + agent.orgChainHealth?.status === "invalid_org_chain"; + const pausedEscalationWarning = !hasInvalidOrgChain + ? (agent.orgChainHealth?.escalationWarning ?? null) + : null; + const showConfigActionBar = + (activeView === "configuration" || + activeView === "instructions" || + activeView === "secrets") && + (configDirty || configSaving); + const showLeftAgentNotice = + agentMembershipState === "left" && !dismissedLeftAgentIds.has(agent.id); const agentMembershipPending = membershipMutation.isPending && membershipMutation.variables?.resourceType === "agent" && membershipMutation.variables.resourceId === agent.id; const agentStarred = isStarred(membershipsQuery.data, "agent", agent.id); - const agentStarPending = agentMembershipPending && membershipMutation.variables?.starred !== undefined; - const agentJoinLeavePending = agentMembershipPending && membershipMutation.variables?.starred === undefined; + const agentStarPending = + agentMembershipPending && + membershipMutation.variables?.starred !== undefined; + const agentJoinLeavePending = + agentMembershipPending && + membershipMutation.variables?.starred === undefined; function handleAgentTabChange(value: string) { if (value === activeView || !prepareAgentNavigation()) return; @@ -1205,7 +1610,9 @@ export function AgentDetail() { } return ( -
+
{showLeftAgentNotice ? (

@@ -1215,26 +1622,36 @@ export function AgentDetail() { compact state="left" pending={agentJoinLeavePending} - pendingState={agentJoinLeavePending ? membershipMutation.variables?.state : null} + pendingState={ + agentJoinLeavePending ? membershipMutation.variables?.state : null + } resourceName={agent.name} - onJoin={() => membershipMutation.mutate({ - resourceType: "agent", - resourceId: agent.id, - resourceName: agent.name, - state: "joined", - })} - onLeave={() => membershipMutation.mutate({ - resourceType: "agent", - resourceId: agent.id, - resourceName: agent.name, - state: "left", - })} + onJoin={() => + membershipMutation.mutate({ + resourceType: "agent", + resourceId: agent.id, + resourceName: agent.name, + state: "joined", + }) + } + onLeave={() => + membershipMutation.mutate({ + resourceType: "agent", + resourceId: agent.id, + resourceName: agent.name, + state: "left", + }) + } /> @@ -1245,7 +1662,9 @@ export function AgentDetail() {

Escalation path is paused

-

{pausedEscalationWarning}

+

+ {pausedEscalationWarning} +

) : null} @@ -1255,16 +1674,20 @@ export function AgentDetail() {

Invalid reporting chain

- {agent.name} cannot accept tasks or start runs until its reporting chain is repaired. + {agent.name} cannot accept tasks or start runs until its reporting + chain is repaired.

{formatOrgChainHealthPath(agent)}

{agent.orgChainHealth?.repairGuidance ? ( -

{agent.orgChainHealth.repairGuidance}

+

+ {agent.orgChainHealth.repairGuidance} +

) : (

- Assign this agent to an active manager/root, or explicitly pause or terminate the affected agent/subtree. + Assign this agent to an active manager/root, or explicitly pause + or terminate the affected agent/subtree.

)}
@@ -1297,25 +1720,30 @@ export function AgentDetail() { starred={agentStarred} pending={agentStarPending} resourceName={agent.name} - onToggle={(next) => membershipMutation.mutate({ - resourceType: "agent", - resourceId: agent.id, - resourceName: agent.name, - starred: next, - })} + onToggle={(next) => + membershipMutation.mutate({ + resourceType: "agent", + resourceId: agent.id, + resourceName: agent.name, + starred: next, + }) + } /> navigate("/agents/all", { replace: true })} + onTerminateSuccess={() => + navigate("/agents/all", { replace: true }) + } hideTerminate={Boolean(builtInState)} pauseConfirm={ builtInState @@ -1323,9 +1751,10 @@ export function AgentDetail() { title: `Pause the ${builtInState.definition.displayName}?`, description: ( <> - {builtInFeatureLabel} depends on this agent. While paused,{" "} - {builtInFeatureLabel.toLowerCase()} generation is skipped and the{" "} - {builtInFeatureLabel} page shows a warning. + {builtInFeatureLabel} depends on this agent. While + paused, {builtInFeatureLabel.toLowerCase()} generation + is skipped and the {builtInFeatureLabel} page shows a + warning. ), } @@ -1341,7 +1770,9 @@ export function AgentDetail() { - Live + + Live + )} @@ -1363,9 +1794,9 @@ export function AgentDetail() { } > - Ships with Paperclip and powers {builtInFeatureLabel}. Configure it like - any agent — model, instructions, budget. It can be paused but not deleted; pausing it - pauses {builtInFeatureLabel}. + Ships with Paperclip and powers {builtInFeatureLabel} + . Configure it like any agent — model, instructions, budget. It can be + paused but not deleted; pausing it pauses {builtInFeatureLabel}. )} @@ -1376,9 +1807,17 @@ export function AgentDetail() { onConfigure={() => setShowBuiltInConfigure(true)} onResetResource={(kind) => resetBuiltInResource.mutate(kind)} onRunRoutine={(routineKey) => runBuiltInRoutine.mutate(routineKey)} - onEnableSchedule={(routineKey) => enableBuiltInSchedule.mutate(routineKey)} - onDisableSchedule={(routineKey) => disableBuiltInSchedule.mutate(routineKey)} - resettingResource={resetBuiltInResource.isPending ? resetBuiltInResource.variables ?? null : null} + onEnableSchedule={(routineKey) => + enableBuiltInSchedule.mutate(routineKey) + } + onDisableSchedule={(routineKey) => + disableBuiltInSchedule.mutate(routineKey) + } + resettingResource={ + resetBuiltInResource.isPending + ? (resetBuiltInResource.variables ?? null) + : null + } routineActionPending={builtInRoutineActionPending} /> )} @@ -1397,10 +1836,7 @@ export function AgentDetail() { )} {!urlRunId && ( - + {actionError}

} {isPendingApproval && (
- This agent is pending board approval and cannot be invoked yet. + + This agent is pending board approval and cannot be invoked yet. +