diff --git a/.agents/skills/company-creator/SKILL.md b/.agents/skills/company-creator/SKILL.md index 7c1aa61b1e..b2f4432bcd 100644 --- a/.agents/skills/company-creator/SKILL.md +++ b/.agents/skills/company-creator/SKILL.md @@ -154,6 +154,14 @@ Each AGENTS.md body should include not just what the agent does, but how they fi This turns a collection of agents into an organization that actually works together. Without workflow context, agents operate in isolation — they do their job but don't know what happens before or after them. +Add a concise execution contract to every generated working agent: + +- Start actionable work in the same heartbeat and do not stop at a plan unless planning was requested. +- Leave durable progress in comments, documents, or work products with the next action. +- Use child issues for long or parallel delegated work instead of polling agents, sessions, or processes. +- Mark blocked work with the unblock owner and action. +- Respect budget, pause/cancel, approval gates, and company boundaries. + ### Step 5: Confirm Output Location Ask the user where to write the package. Common options: diff --git a/.agents/skills/company-creator/references/example-company.md b/.agents/skills/company-creator/references/example-company.md index ba7623b98c..cbc10d28f1 100644 --- a/.agents/skills/company-creator/references/example-company.md +++ b/.agents/skills/company-creator/references/example-company.md @@ -105,6 +105,13 @@ Your responsibilities: - Implement features and fix bugs - Write tests and documentation - Participate in code reviews + +Execution contract: + +- Start actionable implementation work in the same heartbeat; do not stop at a plan unless planning was requested. +- Leave durable progress with a clear next action. +- Use child issues for long or parallel delegated work instead of polling agents, sessions, or processes. +- Mark blocked work with the unblock owner and action. ``` ## teams/engineering/TEAM.md diff --git a/.agents/skills/create-agent-adapter/SKILL.md b/.agents/skills/create-agent-adapter/SKILL.md index dcd6456ee4..a41318580c 100644 --- a/.agents/skills/create-agent-adapter/SKILL.md +++ b/.agents/skills/create-agent-adapter/SKILL.md @@ -548,7 +548,7 @@ Import from `@paperclipai/adapter-utils/server-utils`: ### Prompt Templates - Support `promptTemplate` for every run - Use `renderTemplate()` with the standard variable set -- Default prompt: `"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work."` +- Default prompt should use `DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE` from `@paperclipai/adapter-utils/server-utils` so local adapters share Paperclip's execution contract: act in the same heartbeat, avoid planning-only exits unless requested, leave durable progress and a next action, use child issues instead of polling, mark blockers with owner/action, and respect governance boundaries. ### Error Handling - Differentiate timeout vs process error vs parse failure diff --git a/doc/spec/agents-runtime.md b/doc/spec/agents-runtime.md index 3efeaee287..fd0e44a87d 100644 --- a/doc/spec/agents-runtime.md +++ b/doc/spec/agents-runtime.md @@ -114,14 +114,14 @@ If the connection drops, the UI reconnects automatically. 1. Enable timer wakeups (for example every 300s) 2. Keep assignment wakeups on -3. Use a focused prompt template +3. Use a focused prompt template that tells agents to act in the same heartbeat, leave durable progress, and mark blocked work with an owner/action 4. Watch run logs and adjust prompt/config over time ## 7.2 Event-driven loop (less constant polling) 1. Disable timer or set a long interval 2. Keep wake-on-assignment enabled -3. Use on-demand wakeups for manual nudges +3. Use child issues, comments, and on-demand wakeups for handoffs instead of loops that poll agents, sessions, or processes ## 7.3 Safety-first loop diff --git a/docs/agents-runtime.md b/docs/agents-runtime.md index cafc39e975..81bbda7ca1 100644 --- a/docs/agents-runtime.md +++ b/docs/agents-runtime.md @@ -124,14 +124,14 @@ If the connection drops, the UI reconnects automatically. 1. Enable timer wakeups (for example every 300s) 2. Keep assignment wakeups on -3. Use a focused prompt template +3. Use a focused prompt template that tells agents to act in the same heartbeat, leave durable progress, and mark blocked work with an owner/action 4. Watch run logs and adjust prompt/config over time ## 7.2 Event-driven loop (less constant polling) 1. Disable timer or set a long interval 2. Keep wake-on-assignment enabled -3. Use on-demand wakeups for manual nudges +3. Use child issues, comments, and on-demand wakeups for handoffs instead of loops that poll agents, sessions, or processes ## 7.3 Safety-first loop diff --git a/docs/guides/agent-developer/heartbeat-protocol.md b/docs/guides/agent-developer/heartbeat-protocol.md index 7c24715eb5..b846d7ab97 100644 --- a/docs/guides/agent-developer/heartbeat-protocol.md +++ b/docs/guides/agent-developer/heartbeat-protocol.md @@ -66,7 +66,9 @@ Read ancestors to understand why this task exists. If woken by a specific commen ### Step 7: Do the Work -Use your tools and capabilities to complete the task. +Use your tools and capabilities to complete the task. If the issue is actionable, take a concrete action in the same heartbeat. Do not stop at a plan unless the issue asked for planning. + +Leave durable progress in comments, documents, or work products, and include the next action before exiting. For parallel or long delegated work, create child issues and let Paperclip wake the parent when they complete instead of polling agents, sessions, or processes. ### Step 8: Update Status @@ -102,6 +104,22 @@ Always set `parentId` and `goalId` on subtasks. - **Always checkout** before working — never PATCH to `in_progress` manually - **Never retry a 409** — the task belongs to someone else - **Always comment** on in-progress work before exiting a heartbeat +- **Start actionable work** in the same heartbeat; planning-only exits are for planning tasks +- **Leave a clear next action** in durable issue context +- **Use child issues instead of polling** for long or parallel delegated work - **Always set parentId** on subtasks - **Never cancel cross-team tasks** — reassign to your manager - **Escalate when stuck** — use your chain of command + +## Run Liveness + +Paperclip records run liveness as metadata on heartbeat runs. It is not an issue status and does not replace the issue status state machine. + +- Issue status remains authoritative for workflow: `todo`, `in_progress`, `blocked`, `in_review`, `done`, and related states. +- Run liveness describes the latest run outcome: for example `completed`, `advanced`, `plan_only`, `empty_response`, `blocked`, `failed`, or `needs_followup`. +- Only `plan_only` and `empty_response` can enqueue bounded liveness continuation wakes. +- Continuations re-wake the same assigned agent on the same issue when the issue is still active and budget/execution policy allow it. +- `continuationAttempt` counts semantic liveness continuations for a source run chain. It is separate from process recovery, queued wake delivery, adapter session resume, and other operational retries. +- Liveness continuation wake prompts include the attempt, source run, liveness state, liveness reason, and the instruction for the next heartbeat. +- Continuations do not mark the issue `blocked` or `done`. If automatic continuations are exhausted, Paperclip leaves an audit comment so a human or manager can clarify, block, or assign follow-up work. +- Workspace provisioning alone is not treated as concrete task progress. Durable progress should appear as tool/action events, issue comments, document or work-product revisions, activity log entries, commits, or tests. diff --git a/evals/promptfoo/prompts/heartbeat-system.txt b/evals/promptfoo/prompts/heartbeat-system.txt index 22518b4745..607b4a007e 100644 --- a/evals/promptfoo/prompts/heartbeat-system.txt +++ b/evals/promptfoo/prompts/heartbeat-system.txt @@ -20,6 +20,13 @@ The Heartbeat Procedure: 8. Update status: PATCH /api/issues/{issueId} with status and comment 9. Delegate if needed: POST /api/companies/{companyId}/issues +Execution Contract: +- If the issue is actionable, start concrete work in this heartbeat. Do not stop at a plan unless the issue asks for planning. +- Leave durable progress in comments, documents, or work products, with a clear next action. +- Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes. +- If blocked, PATCH the issue to blocked and name the unblock owner and action. +- Respect budget, pause/cancel, approval gates, and company boundaries. + Critical Rules: - Always checkout before working. Never PATCH to in_progress manually. - Never retry a 409. The task belongs to someone else. diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts index bcdc4ba09e..4f7761858c 100644 --- a/packages/adapter-utils/src/server-utils.test.ts +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -1,6 +1,11 @@ import { randomUUID } from "node:crypto"; import { describe, expect, it } from "vitest"; -import { runChildProcess } from "./server-utils.js"; +import { + DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, + renderPaperclipWakePrompt, + runChildProcess, + stringifyPaperclipWakePayload, +} from "./server-utils.js"; function isPidAlive(pid: number) { try { @@ -21,6 +26,25 @@ async function waitForPidExit(pid: number, timeoutMs = 2_000) { } describe("runChildProcess", () => { + it("does not arm a timeout when timeoutSec is 0", async () => { + const result = await runChildProcess( + randomUUID(), + process.execPath, + ["-e", "setTimeout(() => process.stdout.write('done'), 150);"], + { + cwd: process.cwd(), + env: {}, + timeoutSec: 0, + graceSec: 1, + onLog: async () => {}, + }, + ); + + expect(result.exitCode).toBe(0); + expect(result.timedOut).toBe(false); + expect(result.stdout).toBe("done"); + }); + it("waits for onSpawn before sending stdin to the child", async () => { const spawnDelayMs = 150; const startedAt = Date.now(); @@ -86,3 +110,108 @@ describe("runChildProcess", () => { expect(await waitForPidExit(descendantPid!, 2_000)).toBe(true); }); }); + +describe("renderPaperclipWakePrompt", () => { + it("keeps the default local-agent prompt action-oriented", () => { + expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("Start actionable work in this heartbeat"); + expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("do not stop at a plan"); + expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("Use child issues"); + expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("instead of polling agents, sessions, or processes"); + expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain( + "Respect budget, pause/cancel, approval gates, and company boundaries", + ); + }); + + it("adds the execution contract to scoped wake prompts", () => { + const prompt = renderPaperclipWakePrompt({ + reason: "issue_assigned", + issue: { + id: "issue-1", + identifier: "PAP-1580", + title: "Update prompts", + status: "in_progress", + }, + commentWindow: { + requestedCount: 0, + includedCount: 0, + missingCount: 0, + }, + comments: [], + fallbackFetchNeeded: false, + }); + + expect(prompt).toContain("## Paperclip Wake Payload"); + expect(prompt).toContain("Execution contract: take concrete action in this heartbeat"); + expect(prompt).toContain("use child issues instead of polling"); + expect(prompt).toContain("mark blocked work with the unblock owner/action"); + }); + + it("includes continuation and child issue summaries in structured wake context", () => { + const payload = { + reason: "issue_children_completed", + issue: { + id: "parent-1", + identifier: "PAP-100", + title: "Integrate child work", + status: "in_progress", + priority: "medium", + }, + continuationSummary: { + key: "continuation-summary", + title: "Continuation Summary", + body: "# Continuation Summary\n\n## Next Action\n\n- Integrate child outputs.", + updatedAt: "2026-04-18T12:00:00.000Z", + }, + livenessContinuation: { + attempt: 2, + maxAttempts: 2, + sourceRunId: "run-1", + state: "plan_only", + reason: "Run described future work without concrete action evidence", + instruction: "Take the first concrete action now.", + }, + childIssueSummaries: [ + { + id: "child-1", + identifier: "PAP-101", + title: "Implement helper", + status: "done", + priority: "medium", + summary: "Added the helper route and tests.", + }, + ], + }; + + expect(JSON.parse(stringifyPaperclipWakePayload(payload) ?? "{}")).toMatchObject({ + continuationSummary: { + body: expect.stringContaining("Continuation Summary"), + }, + livenessContinuation: { + attempt: 2, + maxAttempts: 2, + sourceRunId: "run-1", + state: "plan_only", + instruction: "Take the first concrete action now.", + }, + childIssueSummaries: [ + { + identifier: "PAP-101", + summary: "Added the helper route and tests.", + }, + ], + }); + + const prompt = renderPaperclipWakePrompt(payload); + expect(prompt).toContain("Issue continuation summary:"); + expect(prompt).toContain("Integrate child outputs."); + expect(prompt).toContain("Run liveness continuation:"); + expect(prompt).toContain("- attempt: 2/2"); + expect(prompt).toContain("- source run: run-1"); + expect(prompt).toContain("- liveness state: plan_only"); + expect(prompt).toContain("- reason: Run described future work without concrete action evidence"); + expect(prompt).toContain("- instruction: Take the first concrete action now."); + expect(prompt).toContain("Direct child issue summaries:"); + expect(prompt).toContain("PAP-101 Implement helper (done)"); + expect(prompt).toContain("Added the helper route and tests."); + }); +}); diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index ba44249fbd..9078f47b28 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -66,6 +66,17 @@ const PAPERCLIP_SKILL_ROOT_RELATIVE_CANDIDATES = [ "../../../../../skills", ]; +export const DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE = [ + "You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.", + "", + "Execution contract:", + "- Start actionable work in this heartbeat; do not stop at a plan unless the issue asks for planning.", + "- Leave durable progress in comments, documents, or work products with a clear next action.", + "- Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes.", + "- If blocked, mark the issue blocked and name the unblock owner and action.", + "- Respect budget, pause/cancel, approval gates, and company boundaries.", +].join("\n"); + export interface PaperclipSkillEntry { key: string; runtimeName: string; @@ -250,11 +261,41 @@ type PaperclipWakeComment = { authorId: string | null; }; +type PaperclipWakeContinuationSummary = { + key: string | null; + title: string | null; + body: string; + bodyTruncated: boolean; + updatedAt: string | null; +}; + +type PaperclipWakeLivenessContinuation = { + attempt: number | null; + maxAttempts: number | null; + sourceRunId: string | null; + state: string | null; + reason: string | null; + instruction: string | null; +}; + +type PaperclipWakeChildIssueSummary = { + id: string | null; + identifier: string | null; + title: string | null; + status: string | null; + priority: string | null; + summary: string | null; +}; + type PaperclipWakePayload = { reason: string | null; issue: PaperclipWakeIssue | null; checkedOutByHarness: boolean; executionStage: PaperclipWakeExecutionStage | null; + continuationSummary: PaperclipWakeContinuationSummary | null; + livenessContinuation: PaperclipWakeLivenessContinuation | null; + childIssueSummaries: PaperclipWakeChildIssueSummary[]; + childIssueSummaryTruncated: boolean; commentIds: string[]; latestCommentId: string | null; comments: PaperclipWakeComment[]; @@ -298,6 +339,50 @@ function normalizePaperclipWakeComment(value: unknown): PaperclipWakeComment | n }; } +function normalizePaperclipWakeContinuationSummary(value: unknown): PaperclipWakeContinuationSummary | null { + const summary = parseObject(value); + const body = asString(summary.body, "").trim(); + if (!body) return null; + return { + key: asString(summary.key, "").trim() || null, + title: asString(summary.title, "").trim() || null, + body, + bodyTruncated: asBoolean(summary.bodyTruncated, false), + updatedAt: asString(summary.updatedAt, "").trim() || null, + }; +} + +function normalizePaperclipWakeLivenessContinuation(value: unknown): PaperclipWakeLivenessContinuation | null { + const continuation = parseObject(value); + const attempt = asNumber(continuation.attempt, 0); + const maxAttempts = asNumber(continuation.maxAttempts, 0); + const sourceRunId = asString(continuation.sourceRunId, "").trim() || null; + const state = asString(continuation.state, "").trim() || null; + const reason = asString(continuation.reason, "").trim() || null; + const instruction = asString(continuation.instruction, "").trim() || null; + if (!attempt && !maxAttempts && !sourceRunId && !state && !reason && !instruction) return null; + return { + attempt: attempt > 0 ? attempt : null, + maxAttempts: maxAttempts > 0 ? maxAttempts : null, + sourceRunId, + state, + reason, + instruction, + }; +} + +function normalizePaperclipWakeChildIssueSummary(value: unknown): PaperclipWakeChildIssueSummary | null { + const child = parseObject(value); + const id = asString(child.id, "").trim() || null; + const identifier = asString(child.identifier, "").trim() || null; + const title = asString(child.title, "").trim() || null; + const status = asString(child.status, "").trim() || null; + const priority = asString(child.priority, "").trim() || null; + const summary = asString(child.summary, "").trim() || null; + if (!id && !identifier && !title && !status && !summary) return null; + return { id, identifier, title, status, priority, summary }; +} + function normalizePaperclipWakeExecutionPrincipal(value: unknown): PaperclipWakeExecutionPrincipal | null { const principal = parseObject(value); const typeRaw = asString(principal.type, "").trim().toLowerCase(); @@ -356,8 +441,15 @@ export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayl .map((entry) => entry.trim()) : []; const executionStage = normalizePaperclipWakeExecutionStage(payload.executionStage); + const continuationSummary = normalizePaperclipWakeContinuationSummary(payload.continuationSummary); + const livenessContinuation = normalizePaperclipWakeLivenessContinuation(payload.livenessContinuation); + const childIssueSummaries = Array.isArray(payload.childIssueSummaries) + ? payload.childIssueSummaries + .map((entry) => normalizePaperclipWakeChildIssueSummary(entry)) + .filter((entry): entry is PaperclipWakeChildIssueSummary => Boolean(entry)) + : []; - if (comments.length === 0 && commentIds.length === 0 && !executionStage && !normalizePaperclipWakeIssue(payload.issue)) { + if (comments.length === 0 && commentIds.length === 0 && childIssueSummaries.length === 0 && !executionStage && !continuationSummary && !livenessContinuation && !normalizePaperclipWakeIssue(payload.issue)) { return null; } @@ -366,6 +458,10 @@ export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayl issue: normalizePaperclipWakeIssue(payload.issue), checkedOutByHarness: asBoolean(payload.checkedOutByHarness, false), executionStage, + continuationSummary, + livenessContinuation, + childIssueSummaries, + childIssueSummaryTruncated: asBoolean(payload.childIssueSummaryTruncated, false), commentIds, latestCommentId: asString(payload.latestCommentId, "").trim() || null, comments, @@ -406,6 +502,8 @@ export function renderPaperclipWakePrompt( "Focus on the new wake delta below and continue the current task without restating the full heartbeat boilerplate.", "Fetch the API thread only when `fallbackFetchNeeded` is true or you need broader history than this batch.", "", + "Execution contract: take concrete action in this heartbeat when the issue is actionable; do not stop at a plan unless planning was requested. Leave durable progress with a clear next action, use child issues instead of polling for long or parallel work, and mark blocked work with the unblock owner/action.", + "", `- reason: ${normalized.reason ?? "unknown"}`, `- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`, `- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`, @@ -421,6 +519,8 @@ export function renderPaperclipWakePrompt( "Use this inline wake data first before refetching the issue thread.", "Only fetch the API thread when `fallbackFetchNeeded` is true or you need broader history than this batch.", "", + "Execution contract: take concrete action in this heartbeat when the issue is actionable; do not stop at a plan unless planning was requested. Leave durable progress with a clear next action, use child issues instead of polling for long or parallel work, and mark blocked work with the unblock owner/action.", + "", `- reason: ${normalized.reason ?? "unknown"}`, `- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`, `- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`, @@ -470,6 +570,55 @@ export function renderPaperclipWakePrompt( } } + if (normalized.continuationSummary) { + lines.push( + "", + "Issue continuation summary:", + normalized.continuationSummary.body, + ); + if (normalized.continuationSummary.bodyTruncated) { + lines.push("[continuation summary truncated]"); + } + } + + if (normalized.livenessContinuation) { + const continuation = normalized.livenessContinuation; + lines.push("", "Run liveness continuation:"); + if (continuation.attempt) { + lines.push( + `- attempt: ${continuation.attempt}${continuation.maxAttempts ? `/${continuation.maxAttempts}` : ""}`, + ); + } + if (continuation.sourceRunId) { + lines.push(`- source run: ${continuation.sourceRunId}`); + } + if (continuation.state) { + lines.push(`- liveness state: ${continuation.state}`); + } + if (continuation.reason) { + lines.push(`- reason: ${continuation.reason}`); + } + if (continuation.instruction) { + lines.push(`- instruction: ${continuation.instruction}`); + } + } + + if (normalized.childIssueSummaries.length > 0) { + lines.push("", "Direct child issue summaries:"); + for (const child of normalized.childIssueSummaries) { + const label = child.identifier ?? child.id ?? "unknown"; + lines.push( + `- ${label}${child.title ? ` ${child.title}` : ""}${child.status ? ` (${child.status})` : ""}`, + ); + if (child.summary) { + lines.push(` ${child.summary}`); + } + } + if (normalized.childIssueSummaryTruncated) { + lines.push("[child issue summaries truncated]"); + } + } + if (normalized.checkedOutByHarness) { lines.push( "", diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index f2da174e8e..0633a3075f 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -21,6 +21,7 @@ import { renderTemplate, renderPaperclipWakePrompt, stringifyPaperclipWakePayload, + DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, runChildProcess, } from "@paperclipai/adapter-utils/server-utils"; import { @@ -300,7 +301,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise statement-breakpoint +ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "liveness_reason" text;--> statement-breakpoint +ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "continuation_attempt" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "last_useful_action_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "next_action" text;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "heartbeat_runs_company_liveness_idx" ON "heartbeat_runs" USING btree ("company_id","liveness_state","created_at"); diff --git a/packages/db/src/migrations/meta/0058_snapshot.json b/packages/db/src/migrations/meta/0058_snapshot.json new file mode 100644 index 0000000000..713bebfe3e --- /dev/null +++ b/packages/db/src/migrations/meta/0058_snapshot.json @@ -0,0 +1,13490 @@ +{ + "id": "58b07646-6b04-4945-b6d2-e3409d238235", + "prevId": "c13b1dd5-1860-4d0b-aeb2-5bb197766983", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.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 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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" + }, + "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": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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": "no action", + "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.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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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 + }, + "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_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": {} + } + }, + "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 + }, + "dismissed_at": { + "name": "dismissed_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": { + "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.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'" + }, + "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": {}, + "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 + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "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" + } + }, + "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_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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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_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 + }, + "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'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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 + }, + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_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_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" + } + ], + "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": {} + } + }, + "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.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "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.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()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "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_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_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_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 + }, + "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'" + }, + "variables": { + "name": "variables", + "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 + }, + "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": {} + } + }, + "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_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.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.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 + }, + "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": {} + } + }, + "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": "no action", + "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" + } + }, + "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 + }, + "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 757b7c9a14..760e7bc4b7 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -407,6 +407,13 @@ "when": 1776309613598, "tag": "0057_tidy_join_requests", "breakpoints": true + }, + { + "idx": 58, + "version": "7", + "when": 1776542245004, + "tag": "0058_wealthy_starbolt", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/src/schema/heartbeat_runs.ts b/packages/db/src/schema/heartbeat_runs.ts index 4c10050519..6caf37016b 100644 --- a/packages/db/src/schema/heartbeat_runs.ts +++ b/packages/db/src/schema/heartbeat_runs.ts @@ -41,6 +41,11 @@ export const heartbeatRuns = pgTable( issueCommentStatus: text("issue_comment_status").notNull().default("not_applicable"), issueCommentSatisfiedByCommentId: uuid("issue_comment_satisfied_by_comment_id"), issueCommentRetryQueuedAt: timestamp("issue_comment_retry_queued_at", { withTimezone: true }), + livenessState: text("liveness_state"), + livenessReason: text("liveness_reason"), + continuationAttempt: integer("continuation_attempt").notNull().default(0), + lastUsefulActionAt: timestamp("last_useful_action_at", { withTimezone: true }), + nextAction: text("next_action"), contextSnapshot: jsonb("context_snapshot").$type>(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), @@ -51,5 +56,10 @@ export const heartbeatRuns = pgTable( table.agentId, table.startedAt, ), + companyLivenessIdx: index("heartbeat_runs_company_liveness_idx").on( + table.companyId, + table.livenessState, + table.createdAt, + ), }), ); diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index a9f5de3d43..f40f0c830d 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -141,6 +141,16 @@ export type IssueOriginKind = (typeof ISSUE_ORIGIN_KINDS)[number]; export const ISSUE_RELATION_TYPES = ["blocks"] as const; export type IssueRelationType = (typeof ISSUE_RELATION_TYPES)[number]; +export const ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY = "continuation-summary" as const; +export const SYSTEM_ISSUE_DOCUMENT_KEYS = [ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY] as const; +export type SystemIssueDocumentKey = (typeof SYSTEM_ISSUE_DOCUMENT_KEYS)[number]; + +const SYSTEM_ISSUE_DOCUMENT_KEY_SET = new Set(SYSTEM_ISSUE_DOCUMENT_KEYS); + +export function isSystemIssueDocumentKey(key: string): key is SystemIssueDocumentKey { + return SYSTEM_ISSUE_DOCUMENT_KEY_SET.has(key); +} + export const ISSUE_EXECUTION_POLICY_MODES = ["normal", "auto"] as const; export type IssueExecutionPolicyMode = (typeof ISSUE_EXECUTION_POLICY_MODES)[number]; @@ -343,6 +353,17 @@ export const HEARTBEAT_RUN_STATUSES = [ ] as const; export type HeartbeatRunStatus = (typeof HEARTBEAT_RUN_STATUSES)[number]; +export const RUN_LIVENESS_STATES = [ + "completed", + "advanced", + "plan_only", + "empty_response", + "blocked", + "failed", + "needs_followup", +] as const; +export type RunLivenessState = (typeof RUN_LIVENESS_STATES)[number]; + export const LIVE_EVENT_TYPES = [ "heartbeat.run.queued", "heartbeat.run.status", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index d1569b67ab..f4f84ab658 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -16,6 +16,9 @@ export { ISSUE_PRIORITIES, ISSUE_ORIGIN_KINDS, ISSUE_RELATION_TYPES, + ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + SYSTEM_ISSUE_DOCUMENT_KEYS, + isSystemIssueDocumentKey, ISSUE_EXECUTION_POLICY_MODES, ISSUE_EXECUTION_STAGE_TYPES, ISSUE_EXECUTION_STATE_STATUSES, @@ -49,6 +52,7 @@ export { BUDGET_INCIDENT_RESOLUTION_ACTIONS, HEARTBEAT_INVOCATION_SOURCES, HEARTBEAT_RUN_STATUSES, + RUN_LIVENESS_STATES, WAKEUP_TRIGGER_DETAILS, WAKEUP_REQUEST_STATUSES, LIVE_EVENT_TYPES, @@ -93,6 +97,7 @@ export { type IssuePriority, type IssueOriginKind, type IssueRelationType, + type SystemIssueDocumentKey, type IssueExecutionPolicyMode, type IssueExecutionStageType, type IssueExecutionStateStatus, @@ -125,6 +130,7 @@ export { type BudgetIncidentResolutionAction, type HeartbeatInvocationSource, type HeartbeatRunStatus, + type RunLivenessState, type WakeupTriggerDetail, type WakeupRequestStatus, type LiveEventType, @@ -494,6 +500,7 @@ export { type UpdateProjectWorkspace, projectExecutionWorkspacePolicySchema, createIssueSchema, + createChildIssueSchema, createIssueLabelSchema, updateIssueSchema, issueExecutionPolicySchema, @@ -521,6 +528,7 @@ export { upsertIssueDocumentSchema, restoreIssueDocumentRevisionSchema, type CreateIssue, + type CreateChildIssue, type CreateIssueLabel, type UpdateIssue, type CheckoutIssue, diff --git a/packages/shared/src/types/heartbeat.ts b/packages/shared/src/types/heartbeat.ts index 6115f04976..51d15d4262 100644 --- a/packages/shared/src/types/heartbeat.ts +++ b/packages/shared/src/types/heartbeat.ts @@ -3,6 +3,7 @@ import type { AgentStatus, HeartbeatInvocationSource, HeartbeatRunStatus, + RunLivenessState, WakeupTriggerDetail, WakeupRequestStatus, } from "../constants.js"; @@ -38,6 +39,11 @@ export interface HeartbeatRun { processStartedAt: Date | null; retryOfRunId: string | null; processLossRetryCount: number; + livenessState: RunLivenessState | null; + livenessReason: string | null; + continuationAttempt: number; + lastUsefulActionAt: Date | null; + nextAction: string | null; contextSnapshot: Record | null; createdAt: Date; updatedAt: Date; diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index 8e1662f428..ac99dcd51a 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -134,6 +134,7 @@ export { export { createIssueSchema, + createChildIssueSchema, createIssueLabelSchema, updateIssueSchema, issueExecutionPolicySchema, @@ -148,6 +149,7 @@ export { upsertIssueDocumentSchema, restoreIssueDocumentRevisionSchema, type CreateIssue, + type CreateChildIssue, type CreateIssueLabel, type UpdateIssue, type IssueExecutionWorkspaceSettings, diff --git a/packages/shared/src/validators/issue.ts b/packages/shared/src/validators/issue.ts index d1d1e33db9..6b11570dfb 100644 --- a/packages/shared/src/validators/issue.ts +++ b/packages/shared/src/validators/issue.ts @@ -138,6 +138,18 @@ export const createIssueSchema = z.object({ export type CreateIssue = z.infer; +export const createChildIssueSchema = createIssueSchema + .omit({ + parentId: true, + inheritExecutionWorkspaceFromIssueId: true, + }) + .extend({ + acceptanceCriteria: z.array(z.string().trim().min(1).max(500)).max(20).optional(), + blockParentUntilDone: z.boolean().optional().default(false), + }); + +export type CreateChildIssue = z.infer; + export const createIssueLabelSchema = z.object({ name: z.string().trim().min(1).max(48), color: z.string().regex(/^#(?:[0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value"), diff --git a/server/src/__tests__/activity-service.test.ts b/server/src/__tests__/activity-service.test.ts index 2b3bfc37d8..c3c90fabcf 100644 --- a/server/src/__tests__/activity-service.test.ts +++ b/server/src/__tests__/activity-service.test.ts @@ -1,6 +1,17 @@ import { randomUUID } from "node:crypto"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { agents, companies, createDb, heartbeatRuns } from "@paperclipai/db"; +import { + agents, + companies, + createDb, + documentRevisions, + documents, + heartbeatRuns, + issueComments, + issueDocuments, + issues, +} from "@paperclipai/db"; +import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, @@ -9,6 +20,8 @@ import { activityService } from "../services/activity.ts"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; +type ActivityService = ReturnType; +type IssueRun = Awaited>[number]; if (!embeddedPostgresSupport.supported) { console.warn( @@ -16,6 +29,23 @@ if (!embeddedPostgresSupport.supported) { ); } +async function waitForIssueRun( + service: ActivityService, + companyId: string, + issueId: string, + predicate: (run: IssueRun) => boolean, +) { + const deadline = Date.now() + 2_000; + let latestRuns: IssueRun[] = []; + while (Date.now() < deadline) { + latestRuns = await service.runsForIssue(companyId, issueId); + const run = latestRuns.find(predicate); + if (run) return { run, runs: latestRuns }; + await new Promise((resolve) => setTimeout(resolve, 25)); + } + throw new Error(`Timed out waiting for issue run. Latest run count: ${latestRuns.length}`); +} + describeEmbeddedPostgres("activity service", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; @@ -26,6 +56,11 @@ describeEmbeddedPostgres("activity service", () => { }, 20_000); afterEach(async () => { + await db.delete(issueComments); + await db.delete(issueDocuments); + await db.delete(documentRevisions); + await db.delete(documents); + await db.delete(issues); await db.delete(heartbeatRuns); await db.delete(agents); await db.delete(companies); @@ -78,9 +113,17 @@ describeEmbeddedPostgres("activity service", () => { resultJson: { billing_type: "metered", total_cost_usd: 0.42, + stopReason: "timeout", + effectiveTimeoutSec: 30, + timeoutFired: true, summary: "done", nestedHuge: { payload: "y".repeat(256_000) }, }, + livenessState: "advanced", + livenessReason: "Run produced concrete action evidence: 1 issue comment(s)", + continuationAttempt: 2, + lastUsefulActionAt: new Date("2026-04-18T19:59:00.000Z"), + nextAction: "Review the completed output.", }); const runs = await activityService(db).runsForIssue(companyId, issueId); @@ -111,6 +154,337 @@ describeEmbeddedPostgres("activity service", () => { costUsd: 0.42, cost_usd: 0.42, total_cost_usd: 0.42, + stopReason: "timeout", + effectiveTimeoutSec: 30, + timeoutFired: true, + }); + expect(runs[0]).toMatchObject({ + livenessState: "advanced", + livenessReason: "Run produced concrete action evidence: 1 issue comment(s)", + continuationAttempt: 2, + lastUsefulActionAt: new Date("2026-04-18T19:59:00.000Z"), + nextAction: "Review the completed output.", + }); + }); + + it("backfills missing liveness for completed issue runs before returning the ledger", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const runId = randomUUID(); + const completedAt = new Date("2026-04-18T20:04:00.000Z"); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "idle", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Fix run ledger", + description: "Make the run ledger answer whether a run advanced.", + status: "done", + priority: "medium", + assigneeAgentId: agentId, + completedAt, + }); + + await db.insert(heartbeatRuns).values({ + id: runId, + companyId, + agentId, + invocationSource: "assignment", + status: "succeeded", + startedAt: new Date("2026-04-18T20:00:00.000Z"), + finishedAt: completedAt, + contextSnapshot: { issueId }, + resultJson: { + summary: "Finished the implementation.", + }, + livenessState: null, + livenessReason: null, + lastUsefulActionAt: null, + nextAction: null, + }); + + await db.insert(issueComments).values({ + companyId, + issueId, + authorAgentId: agentId, + createdByRunId: runId, + body: "Done", + createdAt: completedAt, + }); + + const service = activityService(db); + const { run, runs } = await waitForIssueRun( + service, + companyId, + issueId, + (entry) => entry.runId === runId && entry.livenessState === "completed", + ); + + expect(runs).toHaveLength(1); + expect(run).toMatchObject({ + runId, + livenessState: "completed", + livenessReason: "Issue is done", + continuationAttempt: 0, + lastUsefulActionAt: completedAt, + }); + + const [persisted] = await db.select().from(heartbeatRuns); + expect(persisted).toMatchObject({ + id: runId, + livenessState: "completed", + livenessReason: "Issue is done", + continuationAttempt: 0, + lastUsefulActionAt: completedAt, + }); + }); + + it("does not backfill document evidence from a different run", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const runId = randomUUID(); + const otherRunId = randomUUID(); + const documentId = randomUUID(); + const revisionId = randomUUID(); + const createdAt = new Date("2026-04-18T20:08:00.000Z"); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "idle", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Fix run ledger", + description: "Make the run ledger answer whether a run advanced.", + status: "in_progress", + priority: "medium", + assigneeAgentId: agentId, + }); + + await db.insert(heartbeatRuns).values([ + { + id: runId, + companyId, + agentId, + invocationSource: "assignment", + status: "succeeded", + startedAt: new Date("2026-04-18T20:00:00.000Z"), + finishedAt: new Date("2026-04-18T20:02:00.000Z"), + contextSnapshot: { issueId }, + resultJson: { + summary: "Next steps:\n- inspect files", + }, + livenessState: null, + livenessReason: null, + }, + { + id: otherRunId, + companyId, + agentId, + invocationSource: "assignment", + status: "succeeded", + startedAt: new Date("2026-04-18T20:05:00.000Z"), + finishedAt: createdAt, + contextSnapshot: { issueId }, + resultJson: { + summary: "Updated the plan document.", + }, + livenessState: "advanced", + livenessReason: "Run produced concrete action evidence: 1 document revision(s)", + }, + ]); + + await db.insert(documents).values({ + id: documentId, + companyId, + title: "Plan", + format: "markdown", + latestBody: "# Plan\n\n- Inspect files", + latestRevisionId: revisionId, + latestRevisionNumber: 1, + createdByAgentId: agentId, + updatedByAgentId: agentId, + createdAt, + updatedAt: createdAt, + }); + + await db.insert(documentRevisions).values({ + id: revisionId, + companyId, + documentId, + revisionNumber: 1, + title: "Plan", + format: "markdown", + body: "# Plan\n\n- Inspect files", + createdByAgentId: agentId, + createdByRunId: otherRunId, + createdAt, + }); + + await db.insert(issueDocuments).values({ + companyId, + issueId, + documentId, + key: "plan", + createdAt, + updatedAt: createdAt, + }); + + const service = activityService(db); + const { run: backfilledRun } = await waitForIssueRun( + service, + companyId, + issueId, + (entry) => entry.runId === runId && entry.livenessState === "plan_only", + ); + + expect(backfilledRun).toMatchObject({ + runId, + livenessState: "plan_only", + livenessReason: "Run described future work without concrete action evidence", + lastUsefulActionAt: null, + }); + }); + + it("does not treat continuation summary revisions as concrete backfill evidence", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const runId = randomUUID(); + const documentId = randomUUID(); + const revisionId = randomUUID(); + const createdAt = new Date("2026-04-18T20:12:00.000Z"); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "idle", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Fix run ledger", + description: "Make the run ledger answer whether a run advanced.", + status: "in_progress", + priority: "medium", + assigneeAgentId: agentId, + }); + + await db.insert(heartbeatRuns).values({ + id: runId, + companyId, + agentId, + invocationSource: "assignment", + status: "succeeded", + startedAt: new Date("2026-04-18T20:10:00.000Z"), + finishedAt: createdAt, + contextSnapshot: { issueId }, + resultJson: { + summary: "Next steps:\n- inspect files", + }, + livenessState: null, + livenessReason: null, + }); + + await db.insert(documents).values({ + id: documentId, + companyId, + title: "Continuation Summary", + format: "markdown", + latestBody: "# Continuation Summary", + latestRevisionId: revisionId, + latestRevisionNumber: 1, + createdByAgentId: agentId, + updatedByAgentId: agentId, + createdAt, + updatedAt: createdAt, + }); + + await db.insert(documentRevisions).values({ + id: revisionId, + companyId, + documentId, + revisionNumber: 1, + title: "Continuation Summary", + format: "markdown", + body: "# Continuation Summary", + createdByAgentId: agentId, + createdByRunId: runId, + createdAt, + }); + + await db.insert(issueDocuments).values({ + companyId, + issueId, + documentId, + key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + createdAt, + updatedAt: createdAt, + }); + + const service = activityService(db); + const { run: backfilledRun } = await waitForIssueRun( + service, + companyId, + issueId, + (entry) => entry.runId === runId && entry.livenessState === "plan_only", + ); + + expect(backfilledRun).toMatchObject({ + runId, + livenessState: "plan_only", + livenessReason: "Run described future work without concrete action evidence", + lastUsefulActionAt: null, }); }); }); diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index 310237b73b..9b8029fc77 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -458,7 +458,7 @@ describe("agent skill routes", () => { adapterType: "claude_local", }), expect.objectContaining({ - "AGENTS.md": expect.stringContaining("Keep the work moving until it's done."), + "AGENTS.md": expect.stringMatching(/Start actionable work in the same heartbeat\.[\s\S]*Keep the work moving until it is done\./), }), { entryFile: "AGENTS.md", replaceExisting: false }, ); diff --git a/server/src/__tests__/documents-service.test.ts b/server/src/__tests__/documents-service.test.ts new file mode 100644 index 0000000000..1265884571 --- /dev/null +++ b/server/src/__tests__/documents-service.test.ts @@ -0,0 +1,115 @@ +import { randomUUID } from "node:crypto"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { + companies, + createDb, + documentRevisions, + documents, + issueDocuments, + issues, +} from "@paperclipai/db"; +import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; +import { documentService } from "../services/documents.js"; + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +if (!embeddedPostgresSupport.supported) { + console.warn( + `Skipping embedded Postgres document service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, + ); +} + +describeEmbeddedPostgres("documentService system issue documents", () => { + let db!: ReturnType; + let svc!: ReturnType; + let tempDb: Awaited> | null = null; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-documents-service-"); + db = createDb(tempDb.connectionString); + svc = documentService(db); + }, 20_000); + + afterEach(async () => { + await db.delete(documentRevisions); + await db.delete(issueDocuments); + await db.delete(documents); + await db.delete(issues); + await db.delete(companies); + }); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + async function createIssueWithDocuments() { + const companyId = randomUUID(); + const issueId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + identifier: "PAP-1600", + title: "System document filtering", + description: "Validate document filtering", + status: "in_progress", + priority: "medium", + }); + + await svc.upsertIssueDocument({ + issueId, + key: "plan", + title: "Plan", + format: "markdown", + body: "# Plan", + }); + await svc.upsertIssueDocument({ + issueId, + key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + title: "Continuation Summary", + format: "markdown", + body: "# Handoff", + }); + + return { issueId }; + } + + it("filters continuation summaries from default document lists and issue payload summaries", async () => { + const { issueId } = await createIssueWithDocuments(); + + const defaultDocuments = await svc.listIssueDocuments(issueId); + expect(defaultDocuments.map((doc) => doc.key)).toEqual(["plan"]); + + const payload = await svc.getIssueDocumentPayload({ id: issueId, description: null }); + expect(payload.planDocument?.key).toBe("plan"); + expect(payload.documentSummaries.map((doc) => doc.key)).toEqual(["plan"]); + }); + + it("keeps system documents available for includeSystem and direct fetch callers", async () => { + const { issueId } = await createIssueWithDocuments(); + + const debugDocuments = await svc.listIssueDocuments(issueId, { includeSystem: true }); + expect(debugDocuments.map((doc) => doc.key)).toEqual([ + ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + "plan", + ]); + + const directHandoff = await svc.getIssueDocumentByKey(issueId, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY); + expect(directHandoff).toEqual(expect.objectContaining({ + key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + body: "# Handoff", + })); + }); +}); diff --git a/server/src/__tests__/heartbeat-list.test.ts b/server/src/__tests__/heartbeat-list.test.ts index a3e6620f72..ccc32c4156 100644 --- a/server/src/__tests__/heartbeat-list.test.ts +++ b/server/src/__tests__/heartbeat-list.test.ts @@ -65,6 +65,11 @@ describeEmbeddedPostgres("heartbeat list", () => { agentId, invocationSource: "assignment", status: "running", + livenessState: "advanced", + livenessReason: "run produced action evidence", + continuationAttempt: 1, + lastUsefulActionAt: new Date("2026-04-18T12:00:00Z"), + nextAction: "continue implementation", contextSnapshot: { issueId: randomUUID() }, }); @@ -80,6 +85,13 @@ describeEmbeddedPostgres("heartbeat list", () => { expect(runs).toHaveLength(1); expect(runs[0]?.id).toBe(runId); expect(runs[0]?.processGroupId ?? null).toBeNull(); + expect(runs[0]).toMatchObject({ + livenessState: "advanced", + livenessReason: "run produced action evidence", + continuationAttempt: 1, + nextAction: "continue implementation", + }); + expect(runs[0]?.lastUsefulActionAt).toEqual(new Date("2026-04-18T12:00:00Z")); } finally { if (originalDescriptor) { Object.defineProperty(heartbeatRuns, "processGroupId", originalDescriptor); diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index 5d907470fa..a85fe711a5 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -10,9 +10,12 @@ import { companySkills, companies, createDb, + documentRevisions, + documents, heartbeatRunEvents, heartbeatRuns, issueComments, + issueDocuments, issues, } from "@paperclipai/db"; import { @@ -22,6 +25,17 @@ import { import { runningProcesses } from "../adapters/index.ts"; const mockTelemetryClient = vi.hoisted(() => ({ track: vi.fn() })); const mockTrackAgentFirstHeartbeat = vi.hoisted(() => vi.fn()); +const mockAdapterExecute = vi.hoisted(() => + vi.fn(async () => ({ + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "Recovered stranded heartbeat work.", + provider: "test", + model: "test-model", + })), +); vi.mock("../telemetry.ts", () => ({ getTelemetryClient: () => mockTelemetryClient, @@ -43,14 +57,7 @@ vi.mock("../adapters/index.ts", async () => { ...actual, getServerAdapter: vi.fn(() => ({ supportsLocalAgentJwt: false, - execute: vi.fn(async () => ({ - exitCode: 0, - signal: null, - timedOut: false, - errorMessage: null, - provider: "test", - model: "test-model", - })), + execute: mockAdapterExecute, })), }; }); @@ -104,6 +111,20 @@ async function waitForRunToSettle( return heartbeat.getRun(runId); } +async function waitForValue( + read: () => Promise, + timeoutMs = 3_000, +) { + const deadline = Date.now() + timeoutMs; + let latest: T | null | undefined = null; + while (Date.now() < deadline) { + latest = await read(); + if (latest) return latest; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + return latest ?? null; +} + async function spawnOrphanedProcessGroup() { const leader = spawn( process.execPath, @@ -157,6 +178,15 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { afterEach(async () => { vi.clearAllMocks(); + mockAdapterExecute.mockImplementation(async () => ({ + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "Recovered stranded heartbeat work.", + provider: "test", + model: "test-model", + })); runningProcesses.clear(); for (const child of childProcesses) { child.kill("SIGKILL"); @@ -170,10 +200,26 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { } } cleanupPids.clear(); - for (let attempt = 0; attempt < 10; attempt += 1) { - const runs = await db.select({ status: heartbeatRuns.status }).from(heartbeatRuns); - if (runs.every((run) => run.status !== "queued" && run.status !== "running")) { - break; + let idlePolls = 0; + for (let attempt = 0; attempt < 100; attempt += 1) { + const runs = await db + .select({ + status: heartbeatRuns.status, + processPid: heartbeatRuns.processPid, + processGroupId: heartbeatRuns.processGroupId, + }) + .from(heartbeatRuns); + const managedExecutionStillActive = runs.some( + (run) => + (run.status === "queued" || run.status === "running") && + !run.processPid && + !run.processGroupId, + ); + if (!managedExecutionStillActive) { + idlePolls += 1; + if (idlePolls >= 3) break; + } else { + idlePolls = 0; } await new Promise((resolve) => setTimeout(resolve, 50)); } @@ -182,6 +228,9 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { await db.delete(agentRuntimeState); await db.delete(companySkills); await db.delete(issueComments); + await db.delete(issueDocuments); + await db.delete(documentRevisions); + await db.delete(documents); await db.delete(issues); await db.delete(heartbeatRunEvents); await db.delete(heartbeatRuns); @@ -439,6 +488,13 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { const retryRun = runs.find((row) => row.id !== runId); expect(failedRun?.status).toBe("failed"); expect(failedRun?.errorCode).toBe("process_lost"); + expect(failedRun?.livenessState).toBe("failed"); + expect(failedRun?.livenessReason).toContain("process_lost"); + expect(failedRun?.resultJson).toMatchObject({ + stopReason: "process_lost", + timeoutConfigured: false, + timeoutFired: false, + }); expect(retryRun?.status).toBe("queued"); expect(retryRun?.retryOfRunId).toBe(runId); expect(retryRun?.processLossRetryCount).toBe(1); @@ -553,6 +609,23 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { ); }); + it("records manual cancellation stop metadata", async () => { + const { runId } = await seedRunFixture({ + agentStatus: "running", + includeIssue: false, + }); + const heartbeat = heartbeatService(db); + + const cancelled = await heartbeat.cancelRun(runId); + expect(cancelled?.status).toBe("cancelled"); + expect(cancelled?.resultJson).toMatchObject({ + stopReason: "cancelled", + effectiveTimeoutSec: 0, + timeoutConfigured: false, + timeoutFired: false, + }); + }); + it("re-enqueues assigned todo work when the last issue run died and no wake remains", async () => { const { agentId, issueId, runId } = await seedStrandedIssueFixture({ status: "todo", @@ -629,6 +702,106 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { } }); + it("classifies actionable plan-only recovery and enqueues one liveness continuation", async () => { + mockAdapterExecute.mockResolvedValueOnce({ + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "I will inspect the repo next and then implement the fix.", + provider: "test", + model: "test-model", + }); + const { agentId, issueId, runId } = await seedStrandedIssueFixture({ + status: "in_progress", + runStatus: "failed", + }); + const heartbeat = heartbeatService(db); + + await heartbeat.reconcileStrandedAssignedIssues(); + + const livenessWake = await waitForValue(async () => { + const rows = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.agentId, agentId)); + return rows.find((row) => row.reason === "run_liveness_continuation") ?? null; + }); + expect(livenessWake).toBeTruthy(); + expect(livenessWake?.payload).toMatchObject({ + issueId, + livenessState: "plan_only", + continuationAttempt: 1, + }); + + const sourceRunId = (livenessWake?.payload as Record | null)?.sourceRunId; + expect(sourceRunId).toBeTruthy(); + const sourceRun = await db + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.id, String(sourceRunId))) + .then((rows) => rows[0] ?? null); + expect(sourceRun?.id).not.toBe(runId); + expect(sourceRun?.livenessState).toBe("plan_only"); + }); + + it("treats a plan document update as progress and does not enqueue liveness continuation", async () => { + const { agentId, companyId, issueId, runId } = await seedStrandedIssueFixture({ + status: "in_progress", + runStatus: "failed", + }); + mockAdapterExecute.mockImplementationOnce(async (ctx: { runId: string }) => { + const documentId = randomUUID(); + const revisionId = randomUUID(); + await db.insert(documents).values({ + id: documentId, + companyId, + title: "Plan", + format: "markdown", + latestBody: "# Plan\n\n- Inspect files\n- Implement fix", + latestRevisionId: revisionId, + latestRevisionNumber: 1, + createdByAgentId: agentId, + updatedByAgentId: agentId, + }); + await db.insert(documentRevisions).values({ + id: revisionId, + companyId, + documentId, + revisionNumber: 1, + title: "Plan", + format: "markdown", + body: "# Plan\n\n- Inspect files\n- Implement fix", + createdByAgentId: agentId, + createdByRunId: ctx.runId, + }); + await db.insert(issueDocuments).values({ + companyId, + issueId, + documentId, + key: "plan", + }); + return { + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "Plan:\n- Inspect files\n- Implement fix", + provider: "test", + model: "test-model", + }; + }); + const heartbeat = heartbeatService(db); + + await heartbeat.reconcileStrandedAssignedIssues(); + + const retryRun = await waitForValue(async () => { + const rows = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, agentId)); + return rows.find((row) => row.id !== runId && row.livenessState === "advanced") ?? null; + }); + expect(retryRun?.livenessState).toBe("advanced"); + + const wakes = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.agentId, agentId)); + expect(wakes.some((row) => row.reason === "run_liveness_continuation")).toBe(false); + }); + it("blocks stranded in-progress work after the continuation retry was already used", async () => { const { issueId } = await seedStrandedIssueFixture({ status: "in_progress", diff --git a/server/src/__tests__/heartbeat-run-summary.test.ts b/server/src/__tests__/heartbeat-run-summary.test.ts index 7355f0d4d8..e5d8848d6c 100644 --- a/server/src/__tests__/heartbeat-run-summary.test.ts +++ b/server/src/__tests__/heartbeat-run-summary.test.ts @@ -15,6 +15,10 @@ describe("summarizeHeartbeatRunResultJson", () => { total_cost_usd: 1.23, cost_usd: 0.45, costUsd: 0.67, + stopReason: "timeout", + effectiveTimeoutSec: 30, + timeoutConfigured: true, + timeoutFired: true, nested: { ignored: true }, }); @@ -26,6 +30,10 @@ describe("summarizeHeartbeatRunResultJson", () => { total_cost_usd: 1.23, cost_usd: 0.45, costUsd: 0.67, + stopReason: "timeout", + effectiveTimeoutSec: 30, + timeoutConfigured: true, + timeoutFired: true, }); }); diff --git a/server/src/__tests__/issue-continuation-summary.test.ts b/server/src/__tests__/issue-continuation-summary.test.ts new file mode 100644 index 0000000000..c5ccd0a574 --- /dev/null +++ b/server/src/__tests__/issue-continuation-summary.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import { + ISSUE_CONTINUATION_SUMMARY_MAX_BODY_CHARS, + buildContinuationSummaryMarkdown, +} from "../services/issue-continuation-summary.js"; + +describe("issue continuation summaries", () => { + it("builds bounded issue-local handoff context with required sections", () => { + const body = buildContinuationSummaryMarkdown({ + issue: { + id: "issue-1", + identifier: "PAP-1579", + title: "Add continuation summaries", + description: [ + "## Objective", + "", + "Keep work resumable after adapter session reset.", + "", + "## Acceptance Criteria", + "", + "- Summary is issue-local", + "- Wake context includes the summary", + ].join("\n"), + status: "in_progress", + priority: "medium", + }, + run: { + id: "run-1", + status: "succeeded", + error: null, + resultJson: { + summary: "Updated server/src/services/heartbeat.ts and packages/adapter-utils/src/server-utils.ts.", + }, + stdoutExcerpt: null, + stderrExcerpt: null, + finishedAt: new Date("2026-04-18T12:00:00.000Z"), + }, + agent: { + id: "agent-1", + name: "CodexCoder", + adapterType: "codex_local", + }, + }); + + expect(body).toContain("# Continuation Summary"); + expect(body).toContain("## Objective"); + expect(body).toContain("Keep work resumable after adapter session reset."); + expect(body).toContain("## Acceptance Criteria"); + expect(body).toContain("- Summary is issue-local"); + expect(body).toContain("## Recent Concrete Actions"); + expect(body).toContain("Run `run-1` finished with status `succeeded`"); + expect(body).toContain("`server/src/services/heartbeat.ts`"); + expect(body).toContain("## Commands Run"); + expect(body).toContain("## Blockers / Decisions"); + expect(body).toContain("## Next Action"); + expect(body.length).toBeLessThanOrEqual(ISSUE_CONTINUATION_SUMMARY_MAX_BODY_CHARS); + }); + + it("uses failure state to point the next run at the error", () => { + const body = buildContinuationSummaryMarkdown({ + issue: { + id: "issue-1", + identifier: "PAP-1579", + title: "Add continuation summaries", + description: null, + status: "in_progress", + priority: "medium", + }, + run: { + id: "run-2", + status: "failed", + error: "adapter failed", + errorCode: "adapter_failed", + resultJson: null, + }, + agent: { + id: "agent-1", + name: "CodexCoder", + adapterType: "codex_local", + }, + }); + + expect(body).toContain("Latest run error (adapter_failed): adapter failed"); + expect(body).toContain("Inspect the failed run, fix the cause"); + }); +}); diff --git a/server/src/__tests__/issue-dependency-wakeups-routes.test.ts b/server/src/__tests__/issue-dependency-wakeups-routes.test.ts index f3ea89eb9e..896253eb77 100644 --- a/server/src/__tests__/issue-dependency-wakeups-routes.test.ts +++ b/server/src/__tests__/issue-dependency-wakeups-routes.test.ts @@ -39,6 +39,7 @@ vi.mock("../services/index.js", () => ({ wakeup: mockWakeup, reportRunActivity: vi.fn(async () => undefined), }), + getIssueContinuationSummaryDocument: vi.fn(async () => null), instanceSettingsService: () => ({ get: vi.fn(), listCompanyIds: vi.fn(), @@ -197,6 +198,31 @@ describe("issue dependency wakeups in issue routes", () => { id: "parent-1", assigneeAgentId: "agent-9", childIssueIds: ["child-0", "child-1"], + childIssueSummaries: [ + { + id: "child-0", + identifier: "PAP-100", + title: "First child", + status: "done", + priority: "medium", + assigneeAgentId: "agent-1", + assigneeUserId: null, + updatedAt: new Date("2026-04-18T12:00:00.000Z"), + summary: "First child finished.", + }, + { + id: "child-1", + identifier: "PAP-101", + title: "Last child", + status: "done", + priority: "medium", + assigneeAgentId: "agent-1", + assigneeUserId: null, + updatedAt: new Date("2026-04-18T12:05:00.000Z"), + summary: "Last child finished.", + }, + ], + childIssueSummaryTruncated: false, }); const res = await request(await createApp()).patch("/api/issues/child-1").send({ status: "done" }); @@ -209,6 +235,14 @@ describe("issue dependency wakeups in issue routes", () => { payload: expect.objectContaining({ issueId: "parent-1", completedChildIssueId: "child-1", + childIssueSummaries: expect.arrayContaining([ + expect.objectContaining({ identifier: "PAP-101", summary: "Last child finished." }), + ]), + }), + contextSnapshot: expect.objectContaining({ + childIssueSummaries: expect.arrayContaining([ + expect.objectContaining({ identifier: "PAP-100", summary: "First child finished." }), + ]), }), }), ); diff --git a/server/src/__tests__/issue-document-restore-routes.test.ts b/server/src/__tests__/issue-document-restore-routes.test.ts index 048cc974fb..a0caa578b3 100644 --- a/server/src/__tests__/issue-document-restore-routes.test.ts +++ b/server/src/__tests__/issue-document-restore-routes.test.ts @@ -10,6 +10,7 @@ const mockIssueService = vi.hoisted(() => ({ })); const mockDocumentsService = vi.hoisted(() => ({ + listIssueDocuments: vi.fn(), listIssueDocumentRevisions: vi.fn(), restoreIssueDocumentRevision: vi.fn(), })); @@ -114,6 +115,25 @@ describe("issue document revision routes", () => { title: "Document revisions", status: "in_progress", }); + mockDocumentsService.listIssueDocuments.mockResolvedValue([ + { + id: "document-1", + companyId, + issueId, + key: "plan", + title: "Plan", + format: "markdown", + body: "# Plan", + latestRevisionId: "revision-2", + latestRevisionNumber: 2, + createdByAgentId: null, + createdByUserId: "board-user", + updatedByAgentId: null, + updatedByUserId: "board-user", + createdAt: new Date("2026-03-26T12:00:00.000Z"), + updatedAt: new Date("2026-03-26T12:10:00.000Z"), + }, + ]); mockDocumentsService.listIssueDocumentRevisions.mockResolvedValue([ { id: "revision-2", @@ -169,6 +189,20 @@ describe("issue document revision routes", () => { ]); }); + it("filters system documents by default on the document list route", async () => { + const res = await request(await createApp()).get(`/api/issues/${issueId}/documents`); + + expect(res.status).toBe(200); + expect(mockDocumentsService.listIssueDocuments).toHaveBeenCalledWith(issueId, { includeSystem: false }); + expect(res.body).toEqual([expect.objectContaining({ key: "plan" })]); + }); + + it("passes includeSystem=true through for debug document listing", async () => { + await request(await createApp()).get(`/api/issues/${issueId}/documents?includeSystem=true`); + + expect(mockDocumentsService.listIssueDocuments).toHaveBeenCalledWith(issueId, { includeSystem: true }); + }); + it("restores a revision through the append-only route and logs the action", async () => { const res = await request(await createApp()) .post(`/api/issues/${issueId}/documents/plan/revisions/revision-1/restore`) diff --git a/server/src/__tests__/issues-goal-context-routes.test.ts b/server/src/__tests__/issues-goal-context-routes.test.ts index c20040840d..d20f5916ea 100644 --- a/server/src/__tests__/issues-goal-context-routes.test.ts +++ b/server/src/__tests__/issues-goal-context-routes.test.ts @@ -22,6 +22,11 @@ const mockGoalService = vi.hoisted(() => ({ getDefaultCompanyGoal: vi.fn(), })); +const mockDocumentsService = vi.hoisted(() => ({ + getIssueDocumentPayload: vi.fn(), + getIssueDocumentByKey: vi.fn(), +})); + vi.mock("../services/index.js", () => ({ accessService: () => ({ canUser: vi.fn(), @@ -30,9 +35,7 @@ vi.mock("../services/index.js", () => ({ agentService: () => ({ getById: vi.fn(), }), - documentService: () => ({ - getIssueDocumentPayload: vi.fn(async () => ({})), - }), + documentService: () => mockDocumentsService, executionWorkspaceService: () => ({ getById: vi.fn(), }), @@ -139,6 +142,8 @@ describe("issue goal context routes", () => { }); mockIssueService.getComment.mockResolvedValue(null); mockIssueService.listAttachments.mockResolvedValue([]); + mockDocumentsService.getIssueDocumentPayload.mockResolvedValue({}); + mockDocumentsService.getIssueDocumentByKey.mockResolvedValue(null); mockProjectService.getById.mockResolvedValue({ id: legacyProjectLinkedIssue.projectId, companyId: "company-1", @@ -214,6 +219,31 @@ describe("issue goal context routes", () => { expect(res.body.attachments).toEqual([]); }); + it("preserves direct continuation summary lookup in GET /issues/:id/heartbeat-context", async () => { + mockDocumentsService.getIssueDocumentByKey.mockResolvedValue({ + key: "continuation-summary", + title: "Continuation Summary", + body: "# Handoff", + latestRevisionId: "revision-1", + latestRevisionNumber: 1, + updatedAt: new Date("2026-04-19T12:00:00.000Z"), + }); + + const res = await request(await createApp()).get( + "/api/issues/11111111-1111-4111-8111-111111111111/heartbeat-context", + ); + + expect(res.status).toBe(200); + expect(mockDocumentsService.getIssueDocumentByKey).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + "continuation-summary", + ); + expect(res.body.continuationSummary).toEqual(expect.objectContaining({ + key: "continuation-summary", + body: "# Handoff", + })); + }); + it("surfaces blocker summaries on GET /issues/:id/heartbeat-context", async () => { mockIssueService.getRelationSummaries.mockResolvedValue({ blockedBy: [ diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 3e233634df..8661e2ab8c 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -8,6 +8,7 @@ import { companies, createDb, executionWorkspaces, + goals, instanceSettings, issueComments, issueInboxArchives, @@ -70,6 +71,7 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { await db.delete(executionWorkspaces); await db.delete(projectWorkspaces); await db.delete(projects); + await db.delete(goals); await db.delete(agents); await db.delete(instanceSettings); await db.delete(companies); @@ -753,6 +755,7 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { await db.delete(executionWorkspaces); await db.delete(projectWorkspaces); await db.delete(projects); + await db.delete(goals); await db.delete(agents); await db.delete(instanceSettings); await db.delete(companies); @@ -1007,6 +1010,104 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { mode: "operator_branch", }); }); + + it("createChild applies parent defaults, acceptance criteria, workspace inheritance, and optional parent blocker chaining", async () => { + const companyId = randomUUID(); + const projectId = randomUUID(); + const goalId = randomUUID(); + const parentIssueId = randomUUID(); + const projectWorkspaceId = randomUUID(); + const executionWorkspaceId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true }); + + await db.insert(goals).values({ + id: goalId, + companyId, + title: "Ship child helpers", + level: "task", + status: "active", + }); + + await db.insert(projects).values({ + id: projectId, + companyId, + goalId, + name: "Workspace project", + status: "in_progress", + }); + + await db.insert(projectWorkspaces).values({ + id: projectWorkspaceId, + companyId, + projectId, + name: "Primary workspace", + isPrimary: true, + }); + + await db.insert(executionWorkspaces).values({ + id: executionWorkspaceId, + companyId, + projectId, + projectWorkspaceId, + mode: "isolated_workspace", + strategyType: "git_worktree", + name: "Issue worktree", + status: "active", + providerType: "git_worktree", + providerRef: `/tmp/${executionWorkspaceId}`, + }); + + await db.insert(issues).values({ + id: parentIssueId, + companyId, + projectId, + projectWorkspaceId, + goalId, + title: "Parent issue", + status: "in_progress", + priority: "medium", + requestDepth: 1, + executionWorkspaceId, + executionWorkspacePreference: "reuse_existing", + executionWorkspaceSettings: { + mode: "isolated_workspace", + }, + }); + + const { issue: child, parentBlockerAdded } = await svc.createChild(parentIssueId, { + title: "Child helper", + status: "todo", + description: "Implement the helper.", + acceptanceCriteria: ["Uses the parent issue as parentId", "Reuses the parent execution workspace"], + blockParentUntilDone: true, + }); + + expect(parentBlockerAdded).toBe(true); + expect(child.parentId).toBe(parentIssueId); + expect(child.projectId).toBe(projectId); + expect(child.goalId).toBe(goalId); + expect(child.requestDepth).toBe(2); + expect(child.description).toContain("## Acceptance Criteria"); + expect(child.description).toContain("- Uses the parent issue as parentId"); + expect(child.projectWorkspaceId).toBe(projectWorkspaceId); + expect(child.executionWorkspaceId).toBe(executionWorkspaceId); + expect(child.executionWorkspacePreference).toBe("reuse_existing"); + + const parentRelations = await svc.getRelationSummaries(parentIssueId); + expect(parentRelations.blockedBy).toEqual([ + expect.objectContaining({ + id: child.id, + title: "Child helper", + }), + ]); + }); }); describeEmbeddedPostgres("issueService blockers and dependency wake readiness", () => { @@ -1208,10 +1309,15 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness", await svc.update(childB, { status: "cancelled" }); - expect(await svc.getWakeableParentAfterChildCompletion(parentId)).toEqual({ + expect(await svc.getWakeableParentAfterChildCompletion(parentId)).toMatchObject({ id: parentId, assigneeAgentId, childIssueIds: [childA, childB], + childIssueSummaries: [ + expect.objectContaining({ id: childA, title: "Child A", status: "done" }), + expect.objectContaining({ id: childB, title: "Child B", status: "cancelled" }), + ], + childIssueSummaryTruncated: false, }); }); }); diff --git a/server/src/__tests__/run-continuations.test.ts b/server/src/__tests__/run-continuations.test.ts new file mode 100644 index 0000000000..a9e1e7b000 --- /dev/null +++ b/server/src/__tests__/run-continuations.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, it } from "vitest"; +import { + DEFAULT_MAX_LIVENESS_CONTINUATION_ATTEMPTS, + RUN_LIVENESS_CONTINUATION_REASON, + buildRunLivenessContinuationIdempotencyKey, + decideRunLivenessContinuation, +} from "../services/run-continuations.ts"; + +const companyId = "company-1"; +const agentId = "agent-1"; +const issueId = "issue-1"; +const runId = "run-1"; + +function run(overrides: Record = {}) { + return { + id: runId, + companyId, + agentId, + continuationAttempt: 0, + ...overrides, + } as never; +} + +function issue(overrides: Record = {}) { + return { + id: issueId, + companyId, + identifier: "PAP-1577", + title: "Add bounded liveness continuation wakes", + status: "in_progress", + assigneeAgentId: agentId, + executionState: null, + projectId: null, + ...overrides, + } as never; +} + +function agent(overrides: Record = {}) { + return { + id: agentId, + companyId, + status: "idle", + ...overrides, + } as never; +} + +describe("run liveness continuations", () => { + it("enqueues the first plan_only continuation for the same issue and assignee", () => { + const decision = decideRunLivenessContinuation({ + run: run(), + issue: issue(), + agent: agent(), + livenessState: "plan_only", + livenessReason: "Planned without acting", + nextAction: "Take the first concrete action now.", + budgetBlocked: false, + idempotentWakeExists: false, + }); + + expect(decision.kind).toBe("enqueue"); + if (decision.kind !== "enqueue") return; + expect(decision.nextAttempt).toBe(1); + expect(decision.idempotencyKey).toBe( + buildRunLivenessContinuationIdempotencyKey({ + issueId, + sourceRunId: runId, + livenessState: "plan_only", + nextAttempt: 1, + }), + ); + expect(decision.payload).toMatchObject({ + issueId, + sourceRunId: runId, + livenessState: "plan_only", + livenessReason: "Planned without acting", + continuationAttempt: 1, + maxContinuationAttempts: DEFAULT_MAX_LIVENESS_CONTINUATION_ATTEMPTS, + instruction: "Take the first concrete action now.", + }); + expect(decision.contextSnapshot).toMatchObject({ + issueId, + wakeReason: RUN_LIVENESS_CONTINUATION_REASON, + livenessContinuationAttempt: 1, + livenessContinuationMaxAttempts: DEFAULT_MAX_LIVENESS_CONTINUATION_ATTEMPTS, + livenessContinuationSourceRunId: runId, + livenessContinuationState: "plan_only", + livenessContinuationReason: "Planned without acting", + livenessContinuationInstruction: "Take the first concrete action now.", + }); + }); + + it("enqueues the second empty_response continuation", () => { + const decision = decideRunLivenessContinuation({ + run: run({ continuationAttempt: 1 }), + issue: issue(), + agent: agent(), + livenessState: "empty_response", + livenessReason: "No useful output", + nextAction: null, + budgetBlocked: false, + idempotentWakeExists: false, + }); + + expect(decision.kind).toBe("enqueue"); + if (decision.kind !== "enqueue") return; + expect(decision.nextAttempt).toBe(2); + }); + + it("does not enqueue a third continuation and returns an exhaustion comment", () => { + const decision = decideRunLivenessContinuation({ + run: run({ continuationAttempt: 2 }), + issue: issue(), + agent: agent(), + livenessState: "plan_only", + livenessReason: "Still planning", + nextAction: null, + budgetBlocked: false, + idempotentWakeExists: false, + }); + + expect(decision.kind).toBe("exhausted"); + if (decision.kind !== "exhausted") return; + expect(decision.comment).toContain("Bounded liveness continuation exhausted"); + expect(decision.comment).toContain("Attempts used: 2/2"); + }); + + it("skips non-actionable and guarded issues", () => { + const guardedCases = [ + { livenessState: "advanced" as const }, + { issue: issue({ status: "done" }) }, + { issue: issue({ assigneeAgentId: "other-agent" }) }, + { issue: issue({ executionState: { status: "pending" } }) }, + { agent: agent({ status: "paused" }) }, + { budgetBlocked: true }, + { idempotentWakeExists: true }, + ]; + + for (const guarded of guardedCases) { + const decision = decideRunLivenessContinuation({ + run: run(), + issue: guarded.issue ?? issue(), + agent: guarded.agent ?? agent(), + livenessState: guarded.livenessState ?? "plan_only", + livenessReason: "No progress", + nextAction: null, + budgetBlocked: guarded.budgetBlocked ?? false, + idempotentWakeExists: guarded.idempotentWakeExists ?? false, + }); + + expect(decision.kind).toBe("skip"); + } + }); +}); diff --git a/server/src/__tests__/run-liveness.test.ts b/server/src/__tests__/run-liveness.test.ts new file mode 100644 index 0000000000..08af19b7d2 --- /dev/null +++ b/server/src/__tests__/run-liveness.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from "vitest"; +import { classifyRunLiveness } from "../services/run-liveness.ts"; + +const baseInput = { + runStatus: "succeeded", + issue: { + status: "in_progress", + title: "Implement feature", + description: "Add the requested behavior.", + }, + resultJson: null, + stdoutExcerpt: null, + stderrExcerpt: null, + error: null, + errorCode: null, + continuationAttempt: 0, + evidence: null, +}; + +describe("run liveness classifier", () => { + it("classifies text-only future work as plan_only", () => { + const classification = classifyRunLiveness({ + ...baseInput, + resultJson: { + summary: "I will inspect the repo next and then implement the fix.", + }, + }); + + expect(classification.livenessState).toBe("plan_only"); + expect(classification.nextAction).toContain("inspect the repo"); + }); + + it("classifies empty successful output as empty_response", () => { + const classification = classifyRunLiveness(baseInput); + + expect(classification.livenessState).toBe("empty_response"); + }); + + it("treats issue comments, documents, products, and actions as progress", () => { + const latestEvidenceAt = new Date("2026-04-18T12:00:00Z"); + const classification = classifyRunLiveness({ + ...baseInput, + resultJson: { + summary: "Updated implementation.", + }, + evidence: { + issueCommentsCreated: 1, + documentRevisionsCreated: 1, + workProductsCreated: 1, + toolOrActionEventsCreated: 1, + latestEvidenceAt, + }, + }); + + expect(classification.livenessState).toBe("advanced"); + expect(classification.lastUsefulActionAt).toBe(latestEvidenceAt); + }); + + it("does not treat workspace operations alone as concrete progress", () => { + const classification = classifyRunLiveness({ + ...baseInput, + resultJson: { + summary: "I will inspect the repo next.", + }, + evidence: { + workspaceOperationsCreated: 1, + latestEvidenceAt: new Date("2026-04-18T12:00:00Z"), + }, + }); + + expect(classification.livenessState).toBe("plan_only"); + expect(classification.lastUsefulActionAt).toBeNull(); + }); + + it("exempts planning/document tasks from plan-only retry classification", () => { + const classification = classifyRunLiveness({ + ...baseInput, + issue: { + status: "in_progress", + title: "Draft implementation plan", + description: "Create a plan for the work.", + }, + resultJson: { + summary: "Plan:\n- Inspect files\n- Implement after approval", + }, + }); + + expect(classification.livenessState).toBe("advanced"); + }); + + it("exempts runs that update the plan document from plan-only classification", () => { + const classification = classifyRunLiveness({ + ...baseInput, + resultJson: { + summary: "Next steps:\n- inspect files\n- implement the service", + }, + evidence: { + documentRevisionsCreated: 1, + planDocumentRevisionsCreated: 1, + latestEvidenceAt: new Date("2026-04-18T12:00:00Z"), + }, + }); + + expect(classification.livenessState).toBe("advanced"); + }); + + it("classifies done issues as completed", () => { + const classification = classifyRunLiveness({ + ...baseInput, + issue: { + ...baseInput.issue, + status: "done", + }, + resultJson: { + summary: "Finished the implementation.", + }, + }); + + expect(classification.livenessState).toBe("completed"); + }); + + it("classifies declared blockers as blocked", () => { + const classification = classifyRunLiveness({ + ...baseInput, + resultJson: { + summary: "I cannot proceed because I need access credentials.", + }, + }); + + expect(classification.livenessState).toBe("blocked"); + }); +}); diff --git a/server/src/adapters/http/execute.test.ts b/server/src/adapters/http/execute.test.ts new file mode 100644 index 0000000000..8565380514 --- /dev/null +++ b/server/src/adapters/http/execute.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { execute } from "./execute.js"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("http adapter execute", () => { + it("reports configured request timeout as timed_out", async () => { + vi.stubGlobal( + "fetch", + vi.fn((_url: string, init?: RequestInit) => new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => { + reject(new DOMException("Aborted", "AbortError")); + }); + })), + ); + + const result = await execute({ + runId: "run-1", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Agent", + adapterType: "http", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + url: "https://example.test/webhook", + timeoutMs: 1, + }, + context: {}, + onLog: async () => {}, + }); + + expect(result.timedOut).toBe(true); + expect(result.errorCode).toBe("timeout"); + expect(result.errorMessage).toContain("timed out after 1ms"); + }); +}); diff --git a/server/src/adapters/http/execute.ts b/server/src/adapters/http/execute.ts index eff140233f..c94b42202a 100644 --- a/server/src/adapters/http/execute.ts +++ b/server/src/adapters/http/execute.ts @@ -36,6 +36,17 @@ export async function execute(ctx: AdapterExecutionContext): Promise`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"), }; @@ -2555,6 +2560,11 @@ export function agentRoutes(db: Db) { agentId: heartbeatRuns.agentId, agentName: agentsTable.name, adapterType: agentsTable.adapterType, + livenessState: heartbeatRuns.livenessState, + livenessReason: heartbeatRuns.livenessReason, + continuationAttempt: heartbeatRuns.continuationAttempt, + lastUsefulActionAt: heartbeatRuns.lastUsefulActionAt, + nextAction: heartbeatRuns.nextAction, }) .from(heartbeatRuns) .innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id)) diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 9d8220a023..5c5067cbb6 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -10,6 +10,7 @@ import { createIssueWorkProductSchema, createIssueLabelSchema, checkoutIssueSchema, + createChildIssueSchema, createIssueSchema, feedbackTargetTypeSchema, feedbackTraceStatusSchema, @@ -17,6 +18,7 @@ import { upsertIssueFeedbackVoteSchema, linkIssueApprovalSchema, issueDocumentKeySchema, + ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, restoreIssueDocumentRevisionSchema, updateIssueWorkProductSchema, upsertIssueDocumentSchema, @@ -769,7 +771,7 @@ export function issueRoutes( ? req.query.wakeCommentId.trim() : null; - const [{ project, goal }, ancestors, commentCursor, wakeComment, relations, attachments] = + const [{ project, goal }, ancestors, commentCursor, wakeComment, relations, attachments, continuationSummary] = await Promise.all([ resolveIssueProjectAndGoal(issue), svc.getAncestors(issue.id), @@ -777,6 +779,7 @@ export function issueRoutes( wakeCommentId ? svc.getComment(wakeCommentId) : null, svc.getRelationSummaries(issue.id), svc.listAttachments(issue.id), + documentsSvc.getIssueDocumentByKey(issue.id, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY), ]); res.json({ @@ -833,6 +836,16 @@ export function issueRoutes( contentPath: withContentPath(a).contentPath, createdAt: a.createdAt, })), + continuationSummary: continuationSummary + ? { + key: continuationSummary.key, + title: continuationSummary.title, + body: continuationSummary.body, + latestRevisionId: continuationSummary.latestRevisionId, + latestRevisionNumber: continuationSummary.latestRevisionNumber, + updatedAt: continuationSummary.updatedAt, + } + : null, }); }); @@ -856,7 +869,9 @@ export function issueRoutes( return; } assertCompanyAccess(req, issue.companyId); - const docs = await documentsSvc.listIssueDocuments(issue.id); + const docs = await documentsSvc.listIssueDocuments(issue.id, { + includeSystem: req.query.includeSystem === "true", + }); res.json(docs); }); @@ -1372,6 +1387,62 @@ export function issueRoutes( res.status(201).json(issue); }); + router.post("/issues/:id/children", validate(createChildIssueSchema), async (req, res) => { + const parentId = req.params.id as string; + const parent = await svc.getById(parentId); + if (!parent) { + res.status(404).json({ error: "Parent issue not found" }); + return; + } + assertCompanyAccess(req, parent.companyId); + assertNoAgentHostWorkspaceCommandMutation(req, collectIssueWorkspaceCommandPaths(req.body)); + if (req.body.assigneeAgentId || req.body.assigneeUserId) { + await assertCanAssignTasks(req, parent.companyId); + } + + const actor = getActorInfo(req); + const executionPolicy = normalizeIssueExecutionPolicy(req.body.executionPolicy); + const { issue, parentBlockerAdded } = await svc.createChild(parent.id, { + ...req.body, + executionPolicy, + createdByAgentId: actor.agentId, + createdByUserId: actor.actorType === "user" ? actor.actorId : null, + actorAgentId: actor.agentId, + actorUserId: actor.actorType === "user" ? actor.actorId : null, + }); + + await logActivity(db, { + companyId: parent.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "issue.child_created", + entityType: "issue", + entityId: issue.id, + details: { + parentId: parent.id, + identifier: issue.identifier, + title: issue.title, + inheritedExecutionWorkspaceFromIssueId: parent.id, + ...(Array.isArray(req.body.blockedByIssueIds) ? { blockedByIssueIds: req.body.blockedByIssueIds } : {}), + ...(parentBlockerAdded ? { parentBlockerAdded: true } : {}), + }, + }); + + void queueIssueAssignmentWakeup({ + heartbeat, + issue, + reason: "issue_assigned", + mutation: "create", + contextSource: "issue.child_create", + requestedByActorType: actor.actorType, + requestedByActorId: actor.actorId, + }); + + res.status(201).json(issue); + }); + router.patch("/issues/:id", validate(updateIssueRouteSchema), async (req, res) => { const id = req.params.id as string; const existing = await svc.getById(id); @@ -1940,6 +2011,8 @@ export function issueRoutes( issueId: parent.id, completedChildIssueId: issue.id, childIssueIds: parent.childIssueIds, + childIssueSummaries: parent.childIssueSummaries, + childIssueSummaryTruncated: parent.childIssueSummaryTruncated, }, requestedByActorType: actor.actorType, requestedByActorId: actor.actorId, @@ -1950,6 +2023,8 @@ export function issueRoutes( source: "issue.children_completed", completedChildIssueId: issue.id, childIssueIds: parent.childIssueIds, + childIssueSummaries: parent.childIssueSummaries, + childIssueSummaryTruncated: parent.childIssueSummaryTruncated, }, }); } diff --git a/server/src/services/activity.ts b/server/src/services/activity.ts index e467ff316d..2198f19be7 100644 --- a/server/src/services/activity.ts +++ b/server/src/services/activity.ts @@ -1,6 +1,20 @@ import { and, desc, eq, isNull, or, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; -import { activityLog, agents, heartbeatRuns, issues } from "@paperclipai/db"; +import { + activityLog, + agents, + documentRevisions, + heartbeatRunEvents, + heartbeatRuns, + issueComments, + issueDocuments, + issues, + issueWorkProducts, + workspaceOperations, +} from "@paperclipai/db"; +import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared"; +import { logger } from "../middleware/logger.js"; +import { classifyRunLiveness } from "./run-liveness.js"; export interface ActivityFilters { companyId: string; @@ -10,6 +24,7 @@ export interface ActivityFilters { } export function activityService(db: Db) { + const scheduledLivenessBackfills = new Set(); const issueIdAsText = sql`${issues.id}::text`; const summarizedUsageJson = sql | null>` case @@ -74,11 +89,230 @@ export function activityService(db: Db) { ${heartbeatRuns.resultJson} -> 'total_cost_usd', ${heartbeatRuns.resultJson} -> 'cost_usd', ${heartbeatRuns.resultJson} -> 'costUsd' - ) + ), + 'stopReason', ${heartbeatRuns.resultJson} -> 'stopReason', + 'effectiveTimeoutSec', ${heartbeatRuns.resultJson} -> 'effectiveTimeoutSec', + 'effectiveTimeoutMs', ${heartbeatRuns.resultJson} -> 'effectiveTimeoutMs', + 'timeoutConfigured', ${heartbeatRuns.resultJson} -> 'timeoutConfigured', + 'timeoutSource', ${heartbeatRuns.resultJson} -> 'timeoutSource', + 'timeoutFired', ${heartbeatRuns.resultJson} -> 'timeoutFired' )) end `.as("resultJson"); + function countValue(value: unknown) { + const parsed = Number(value ?? 0); + return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0; + } + + function dateValue(value: unknown) { + 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; + } + return null; + } + + function latestDate(...values: unknown[]) { + let latest: Date | null = null; + for (const value of values) { + const parsed = dateValue(value); + if (!parsed) continue; + if (!latest || parsed.getTime() > latest.getTime()) latest = parsed; + } + return latest; + } + + function asRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; + } + + function readNumber(value: unknown) { + return typeof value === "number" && Number.isFinite(value) ? value : null; + } + + async function backfillMissingRunLivenessForIssue(companyId: string, issueId: string) { + const runs = await db + .select({ + id: heartbeatRuns.id, + companyId: heartbeatRuns.companyId, + status: heartbeatRuns.status, + contextSnapshot: heartbeatRuns.contextSnapshot, + resultJson: heartbeatRuns.resultJson, + stdoutExcerpt: heartbeatRuns.stdoutExcerpt, + stderrExcerpt: heartbeatRuns.stderrExcerpt, + error: heartbeatRuns.error, + errorCode: heartbeatRuns.errorCode, + continuationAttempt: heartbeatRuns.continuationAttempt, + }) + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.companyId, companyId), + isNull(heartbeatRuns.livenessState), + sql`${heartbeatRuns.status} not in ('queued', 'running')`, + or( + sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}`, + sql`exists ( + select 1 + from ${activityLog} + where ${activityLog.companyId} = ${companyId} + and ${activityLog.entityType} = 'issue' + and ${activityLog.entityId} = ${issueId} + and ${activityLog.runId} = ${heartbeatRuns.id} + )`, + ), + ), + ) + .limit(20); + + if (runs.length === 0) return; + + const issue = await db + .select({ + status: issues.status, + title: issues.title, + description: issues.description, + }) + .from(issues) + .where(and(eq(issues.companyId, companyId), eq(issues.id, issueId))) + .then((rows) => rows[0] ?? null); + + for (const run of runs) { + const context = asRecord(run.contextSnapshot); + const continuationAttempt = + readNumber(context?.continuationAttempt) ?? + readNumber(context?.livenessContinuationAttempt) ?? + run.continuationAttempt ?? + 0; + + const [commentStats] = await db + .select({ + count: sql`count(*)::int`, + latestAt: sql`max(${issueComments.createdAt})`, + }) + .from(issueComments) + .where( + and( + eq(issueComments.companyId, companyId), + eq(issueComments.issueId, issueId), + eq(issueComments.createdByRunId, run.id), + ), + ); + + const [documentStats] = 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, companyId), + eq(documentRevisions.createdByRunId, run.id), + eq(issueDocuments.companyId, companyId), + eq(issueDocuments.issueId, issueId), + sql`${issueDocuments.key} != ${ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY}`, + ), + ); + + const [workProductStats] = await db + .select({ + count: sql`count(*)::int`, + latestAt: sql`max(${issueWorkProducts.createdAt})`, + }) + .from(issueWorkProducts) + .where( + and( + eq(issueWorkProducts.companyId, companyId), + eq(issueWorkProducts.issueId, issueId), + eq(issueWorkProducts.createdByRunId, run.id), + ), + ); + + const [workspaceOperationStats] = await db + .select({ + count: sql`count(*)::int`, + latestAt: sql`max(${workspaceOperations.startedAt})`, + }) + .from(workspaceOperations) + .where(and(eq(workspaceOperations.companyId, companyId), eq(workspaceOperations.heartbeatRunId, run.id))); + + const [activityStats] = await db + .select({ + count: sql`count(*)::int`, + latestAt: sql`max(${activityLog.createdAt})`, + }) + .from(activityLog) + .where(and(eq(activityLog.companyId, companyId), eq(activityLog.runId, run.id))); + + const [eventStats] = await db + .select({ + count: sql`count(*) filter (where ${heartbeatRunEvents.eventType} not in ('lifecycle', 'adapter.invoke', 'error'))::int`, + latestAt: sql`max(${heartbeatRunEvents.createdAt}) filter (where ${heartbeatRunEvents.eventType} not in ('lifecycle', 'adapter.invoke', 'error'))`, + }) + .from(heartbeatRunEvents) + .where(and(eq(heartbeatRunEvents.companyId, companyId), eq(heartbeatRunEvents.runId, run.id))); + + const classification = classifyRunLiveness({ + runStatus: run.status, + issue, + resultJson: asRecord(run.resultJson), + stdoutExcerpt: run.stdoutExcerpt, + stderrExcerpt: run.stderrExcerpt, + error: run.error, + errorCode: run.errorCode, + continuationAttempt, + evidence: { + issueCommentsCreated: countValue(commentStats?.count), + documentRevisionsCreated: countValue(documentStats?.count), + planDocumentRevisionsCreated: countValue(documentStats?.planCount), + workProductsCreated: countValue(workProductStats?.count), + workspaceOperationsCreated: countValue(workspaceOperationStats?.count), + activityEventsCreated: countValue(activityStats?.count), + toolOrActionEventsCreated: countValue(eventStats?.count), + latestEvidenceAt: latestDate( + commentStats?.latestAt, + documentStats?.latestAt, + workProductStats?.latestAt, + workspaceOperationStats?.latestAt, + activityStats?.latestAt, + eventStats?.latestAt, + ), + }, + }); + + await db + .update(heartbeatRuns) + .set({ + livenessState: classification.livenessState, + livenessReason: classification.livenessReason, + continuationAttempt: classification.continuationAttempt, + lastUsefulActionAt: classification.lastUsefulActionAt, + nextAction: classification.nextAction, + updatedAt: new Date(), + }) + .where(and(eq(heartbeatRuns.id, run.id), isNull(heartbeatRuns.livenessState))); + } + } + + function scheduleRunLivenessBackfill(companyId: string, issueId: string) { + const key = `${companyId}:${issueId}`; + if (scheduledLivenessBackfills.has(key)) return; + scheduledLivenessBackfills.add(key); + void backfillMissingRunLivenessForIssue(companyId, issueId) + .catch((err: unknown) => { + logger.warn({ err, companyId, issueId }, "run liveness backfill failed"); + }) + .finally(() => { + scheduledLivenessBackfills.delete(key); + }); + } + return { list: (filters: ActivityFilters) => { const conditions = [eq(activityLog.companyId, filters.companyId)]; @@ -128,8 +362,9 @@ export function activityService(db: Db) { ) .orderBy(desc(activityLog.createdAt)), - runsForIssue: (companyId: string, issueId: string) => - db + runsForIssue: async (companyId: string, issueId: string) => { + scheduleRunLivenessBackfill(companyId, issueId); + return db .select({ runId: heartbeatRuns.id, status: heartbeatRuns.status, @@ -142,6 +377,11 @@ export function activityService(db: Db) { usageJson: summarizedUsageJson, resultJson: summarizedResultJson, logBytes: heartbeatRuns.logBytes, + livenessState: heartbeatRuns.livenessState, + livenessReason: heartbeatRuns.livenessReason, + continuationAttempt: heartbeatRuns.continuationAttempt, + lastUsefulActionAt: heartbeatRuns.lastUsefulActionAt, + nextAction: heartbeatRuns.nextAction, }) .from(heartbeatRuns) .innerJoin( @@ -167,7 +407,8 @@ export function activityService(db: Db) { ), ), ) - .orderBy(desc(heartbeatRuns.createdAt)), + .orderBy(desc(heartbeatRuns.createdAt)); + }, issuesForRun: async (runId: string) => { const run = await db diff --git a/server/src/services/documents.ts b/server/src/services/documents.ts index 1a56af27d1..5c5ee74761 100644 --- a/server/src/services/documents.ts +++ b/server/src/services/documents.ts @@ -1,7 +1,7 @@ import { and, asc, desc, eq } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { documentRevisions, documents, issueDocuments, issues } from "@paperclipai/db"; -import { issueDocumentKeySchema } from "@paperclipai/shared"; +import { isSystemIssueDocumentKey, issueDocumentKeySchema } from "@paperclipai/shared"; import { conflict, notFound, unprocessable } from "../errors.js"; function normalizeDocumentKey(key: string) { @@ -83,8 +83,14 @@ const issueDocumentSelect = { }; export function documentService(db: Db) { + const filterSystemDocuments = (rows: T[], includeSystem: boolean) => + includeSystem ? rows : rows.filter((row) => !isSystemIssueDocumentKey(row.key)); + return { - getIssueDocumentPayload: async (issue: { id: string; description: string | null }) => { + getIssueDocumentPayload: async ( + issue: { id: string; description: string | null }, + options: { includeSystem?: boolean } = {}, + ) => { const [planDocument, documentSummaries] = await Promise.all([ db .select(issueDocumentSelect) @@ -104,7 +110,8 @@ export function documentService(db: Db) { return { planDocument: planDocument ? mapIssueDocumentRow(planDocument, true) : null, - documentSummaries: documentSummaries.map((row) => mapIssueDocumentRow(row, false)), + documentSummaries: filterSystemDocuments(documentSummaries, options.includeSystem ?? false) + .map((row) => mapIssueDocumentRow(row, false)), legacyPlanDocument: legacyPlanBody ? { key: "plan" as const, @@ -115,14 +122,14 @@ export function documentService(db: Db) { }; }, - listIssueDocuments: async (issueId: string) => { + listIssueDocuments: async (issueId: string, options: { includeSystem?: boolean } = {}) => { const rows = await db .select(issueDocumentSelect) .from(issueDocuments) .innerJoin(documents, eq(issueDocuments.documentId, documents.id)) .where(eq(issueDocuments.issueId, issueId)) .orderBy(asc(issueDocuments.key), desc(documents.updatedAt)); - return rows.map((row) => mapIssueDocumentRow(row, true)); + return filterSystemDocuments(rows, options.includeSystem ?? false).map((row) => mapIssueDocumentRow(row, true)); }, getIssueDocumentByKey: async (issueId: string, rawKey: string) => { diff --git a/server/src/services/heartbeat-run-summary.ts b/server/src/services/heartbeat-run-summary.ts index c071e867e7..479957dce2 100644 --- a/server/src/services/heartbeat-run-summary.ts +++ b/server/src/services/heartbeat-run-summary.ts @@ -69,6 +69,26 @@ export function summarizeHeartbeatRunResultJson( } } + for (const key of ["stopReason", "timeoutSource"] as const) { + const value = readCommentText(resultJson[key]); + if (value !== null) { + summary[key] = value; + } + } + + for (const key of ["effectiveTimeoutSec", "effectiveTimeoutMs"] as const) { + const value = readNumericField(resultJson, key); + if (value !== undefined && value !== null) { + summary[key] = value; + } + } + + for (const key of ["timeoutConfigured", "timeoutFired"] as const) { + if (typeof resultJson[key] === "boolean") { + summary[key] = resultJson[key]; + } + } + return Object.keys(summary).length > 0 ? summary : null; } diff --git a/server/src/services/heartbeat-stop-metadata.test.ts b/server/src/services/heartbeat-stop-metadata.test.ts new file mode 100644 index 0000000000..fa0b636613 --- /dev/null +++ b/server/src/services/heartbeat-stop-metadata.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import { + buildHeartbeatRunStopMetadata, + mergeHeartbeatRunStopMetadata, + resolveHeartbeatRunTimeoutPolicy, +} from "./heartbeat-stop-metadata.js"; + +describe("heartbeat stop metadata", () => { + it("keeps local coding adapters at no timeout by default", () => { + for (const adapterType of [ + "codex_local", + "claude_local", + "cursor", + "gemini_local", + "opencode_local", + "pi_local", + "process", + ]) { + expect(resolveHeartbeatRunTimeoutPolicy(adapterType, {})).toEqual({ + effectiveTimeoutSec: 0, + timeoutConfigured: false, + timeoutSource: "default", + }); + } + }); + + it("records configured timeout policy and timeout stop reason", () => { + const metadata = buildHeartbeatRunStopMetadata({ + adapterType: "codex_local", + adapterConfig: { timeoutSec: 45 }, + outcome: "timed_out", + errorCode: "timeout", + errorMessage: "Timed out after 45s", + }); + + expect(metadata).toEqual({ + effectiveTimeoutSec: 45, + timeoutConfigured: true, + timeoutSource: "config", + stopReason: "timeout", + timeoutFired: true, + }); + }); + + it("distinguishes budget cancellation from manual cancellation", () => { + expect( + buildHeartbeatRunStopMetadata({ + adapterType: "codex_local", + adapterConfig: {}, + outcome: "cancelled", + errorCode: "cancelled", + errorMessage: "Cancelled due to budget pause", + }).stopReason, + ).toBe("budget_paused"); + + expect( + buildHeartbeatRunStopMetadata({ + adapterType: "codex_local", + adapterConfig: {}, + outcome: "cancelled", + errorCode: "cancelled", + errorMessage: "Cancelled by control plane", + }).stopReason, + ).toBe("cancelled"); + }); + + it("preserves existing result fields when merging stop metadata", () => { + const result = mergeHeartbeatRunStopMetadata( + { summary: "done" }, + buildHeartbeatRunStopMetadata({ + adapterType: "openclaw_gateway", + adapterConfig: {}, + outcome: "succeeded", + }), + ); + + expect(result).toMatchObject({ + summary: "done", + stopReason: "completed", + effectiveTimeoutSec: 120, + timeoutConfigured: true, + timeoutSource: "default", + timeoutFired: false, + }); + }); +}); diff --git a/server/src/services/heartbeat-stop-metadata.ts b/server/src/services/heartbeat-stop-metadata.ts new file mode 100644 index 0000000000..e04c3b9ffa --- /dev/null +++ b/server/src/services/heartbeat-stop-metadata.ts @@ -0,0 +1,119 @@ +export type HeartbeatRunOutcome = "succeeded" | "failed" | "cancelled" | "timed_out"; + +export type HeartbeatRunStopReason = + | "completed" + | "timeout" + | "cancelled" + | "budget_paused" + | "paused" + | "process_lost" + | "adapter_failed"; + +export interface HeartbeatRunTimeoutPolicy { + effectiveTimeoutSec: number | null; + effectiveTimeoutMs?: number | null; + timeoutConfigured: boolean; + timeoutSource: "config" | "default" | "unknown"; +} + +export interface HeartbeatRunStopMetadata extends HeartbeatRunTimeoutPolicy { + stopReason: HeartbeatRunStopReason; + timeoutFired: boolean; +} + +function readFiniteNumber(value: unknown): number | null { + if (typeof value === "number") { + return Number.isFinite(value) ? value : null; + } + if (typeof value === "string") { + const parsed = Number(value.trim()); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} + +function hasOwn(record: Record, key: string) { + return Object.prototype.hasOwnProperty.call(record, key); +} + +function defaultTimeoutSecForAdapter(adapterType: string) { + return adapterType === "openclaw_gateway" ? 120 : 0; +} + +export function resolveHeartbeatRunTimeoutPolicy( + adapterType: string, + adapterConfig: Record | null | undefined, +): HeartbeatRunTimeoutPolicy { + const config = adapterConfig ?? {}; + + if (adapterType === "http") { + const hasTimeoutMs = hasOwn(config, "timeoutMs"); + const rawTimeoutMs = hasTimeoutMs ? readFiniteNumber(config.timeoutMs) : 0; + const timeoutMs = Math.max(0, Math.floor(rawTimeoutMs ?? 0)); + return { + effectiveTimeoutSec: timeoutMs / 1000, + effectiveTimeoutMs: timeoutMs, + timeoutConfigured: timeoutMs > 0, + timeoutSource: hasTimeoutMs ? "config" : "default", + }; + } + + const hasTimeoutSec = hasOwn(config, "timeoutSec"); + const defaultTimeoutSec = defaultTimeoutSecForAdapter(adapterType); + const rawTimeoutSec = hasTimeoutSec ? readFiniteNumber(config.timeoutSec) : defaultTimeoutSec; + const timeoutSec = Math.max(0, Math.floor(rawTimeoutSec ?? defaultTimeoutSec)); + + return { + effectiveTimeoutSec: timeoutSec, + timeoutConfigured: timeoutSec > 0, + timeoutSource: hasTimeoutSec ? "config" : "default", + }; +} + +export function inferHeartbeatRunStopReason(input: { + outcome: HeartbeatRunOutcome; + errorCode?: string | null; + errorMessage?: string | null; +}): HeartbeatRunStopReason { + if (input.outcome === "succeeded") return "completed"; + if (input.outcome === "timed_out") return "timeout"; + if (input.outcome === "failed" && input.errorCode === "process_lost") return "process_lost"; + if (input.outcome === "cancelled") { + const message = (input.errorMessage ?? "").toLowerCase(); + if (message.includes("budget")) return "budget_paused"; + if (message.includes("pause") || message.includes("paused")) return "paused"; + return "cancelled"; + } + return "adapter_failed"; +} + +export function buildHeartbeatRunStopMetadata(input: { + adapterType: string; + adapterConfig: Record | null | undefined; + outcome: HeartbeatRunOutcome; + errorCode?: string | null; + errorMessage?: string | null; +}): HeartbeatRunStopMetadata { + const timeoutPolicy = resolveHeartbeatRunTimeoutPolicy(input.adapterType, input.adapterConfig); + const stopReason = inferHeartbeatRunStopReason(input); + return { + ...timeoutPolicy, + stopReason, + timeoutFired: stopReason === "timeout", + }; +} + +export function mergeHeartbeatRunStopMetadata( + resultJson: Record | null | undefined, + metadata: HeartbeatRunStopMetadata, +): Record { + return { + ...(resultJson ?? {}), + stopReason: metadata.stopReason, + effectiveTimeoutSec: metadata.effectiveTimeoutSec, + timeoutConfigured: metadata.timeoutConfigured, + timeoutSource: metadata.timeoutSource, + timeoutFired: metadata.timeoutFired, + ...(metadata.effectiveTimeoutMs != null ? { effectiveTimeoutMs: metadata.effectiveTimeoutMs } : {}), + }; +} diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 02d108bb65..f6a423f9ce 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -4,19 +4,25 @@ import { execFile as execFileCallback } from "node:child_process"; import { promisify } from "node:util"; import { and, asc, desc, eq, getTableColumns, gt, inArray, isNull, or, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; -import type { BillingType, ExecutionWorkspace, ExecutionWorkspaceConfig } from "@paperclipai/shared"; +import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared"; +import type { BillingType, ExecutionWorkspace, ExecutionWorkspaceConfig, RunLivenessState } from "@paperclipai/shared"; import { agents, agentRuntimeState, agentTaskSessions, agentWakeupRequests, + activityLog, companySkills as companySkillsTable, + documentRevisions, + issueDocuments, heartbeatRunEvents, heartbeatRuns, issueComments, issues, + issueWorkProducts, projects, projectWorkspaces, + workspaceOperations, } from "@paperclipai/db"; import { conflict, HttpError, notFound } from "../errors.js"; import { logger } from "../middleware/logger.js"; @@ -40,6 +46,14 @@ import { HEARTBEAT_RUN_SAFE_RESULT_JSON_MAX_BYTES, mergeHeartbeatRunResultJson, } from "./heartbeat-run-summary.js"; +import { + buildHeartbeatRunStopMetadata, + mergeHeartbeatRunStopMetadata, +} from "./heartbeat-stop-metadata.js"; +import { + classifyRunLiveness, + type RunLivenessClassificationInput, +} from "./run-liveness.js"; import { logActivity, type LogActivityInput } from "./activity-log.js"; import { buildWorkspaceReadyComment, @@ -53,6 +67,10 @@ import { sanitizeRuntimeServiceBaseEnv, } from "./workspace-runtime.js"; import { issueService } from "./issues.js"; +import { + getIssueContinuationSummaryDocument, + refreshIssueContinuationSummary, +} from "./issue-continuation-summary.js"; import { executionWorkspaceService, mergeExecutionWorkspaceConfig } from "./execution-workspaces.js"; import { workspaceOperationService } from "./workspace-operations.js"; import { isProcessGroupAlive, terminateLocalService } from "./local-service-supervisor.js"; @@ -65,6 +83,13 @@ import { resolveExecutionWorkspaceMode, } from "./execution-workspace-policy.js"; import { instanceSettingsService } from "./instance-settings.js"; +import { + RUN_LIVENESS_CONTINUATION_REASON, + buildRunLivenessContinuationIdempotencyKey, + decideRunLivenessContinuation, + findExistingRunLivenessContinuationWake, + readContinuationAttempt, +} from "./run-continuations.js"; import { redactCurrentUserText, redactCurrentUserValue } from "../log-redaction.js"; import { hasSessionCompactionThresholds, @@ -397,6 +422,11 @@ const heartbeatRunListColumns = { processStartedAt: heartbeatRuns.processStartedAt, retryOfRunId: heartbeatRuns.retryOfRunId, processLossRetryCount: heartbeatRuns.processLossRetryCount, + livenessState: heartbeatRuns.livenessState, + livenessReason: heartbeatRuns.livenessReason, + continuationAttempt: heartbeatRuns.continuationAttempt, + lastUsefulActionAt: heartbeatRuns.lastUsefulActionAt, + nextAction: heartbeatRuns.nextAction, createdAt: heartbeatRuns.createdAt, updatedAt: heartbeatRuns.updatedAt, } as const; @@ -490,6 +520,11 @@ const heartbeatRunIssueSummaryColumns = { finishedAt: heartbeatRuns.finishedAt, createdAt: heartbeatRuns.createdAt, agentId: heartbeatRuns.agentId, + livenessState: heartbeatRuns.livenessState, + livenessReason: heartbeatRuns.livenessReason, + continuationAttempt: heartbeatRuns.continuationAttempt, + lastUsefulActionAt: heartbeatRuns.lastUsefulActionAt, + nextAction: heartbeatRuns.nextAction, issueId: sql`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"), } as const; @@ -1204,6 +1239,14 @@ async function buildPaperclipWakePayload(input: { db: Db; companyId: string; contextSnapshot: Record; + continuationSummary?: + | { + key: string; + title: string | null; + body: string; + updatedAt: Date; + } + | null; issueSummary?: | { id: string; @@ -1217,6 +1260,7 @@ async function buildPaperclipWakePayload(input: { const executionStage = parseObject(input.contextSnapshot.executionStage); const commentIds = extractWakeCommentIds(input.contextSnapshot); const issueId = readNonEmptyString(input.contextSnapshot.issueId); + const continuationSummary = input.continuationSummary ?? null; const issueSummary = input.issueSummary ?? (issueId @@ -1309,8 +1353,37 @@ async function buildPaperclipWakePayload(input: { priority: issueSummary.priority, } : null, + 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) || + 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, checkedOutByHarness: input.contextSnapshot[PAPERCLIP_HARNESS_CHECKOUT_KEY] === true, executionStage: Object.keys(executionStage).length > 0 ? executionStage : null, + continuationSummary: continuationSummary + ? { + key: continuationSummary.key, + title: continuationSummary.title, + body: + continuationSummary.body.length > 4_000 + ? continuationSummary.body.slice(0, 4_000) + : continuationSummary.body, + bodyTruncated: continuationSummary.body.length > 4_000, + updatedAt: continuationSummary.updatedAt.toISOString(), + } + : null, commentIds, latestCommentId: commentIds[commentIds.length - 1] ?? null, comments, @@ -1643,6 +1716,7 @@ export function heartbeatService(db: Db) { agent: typeof agents.$inferSelect; sessionId: string | null; issueId: string | null; + continuationSummaryBody?: string | null; }): Promise { const { agent, sessionId, issueId } = input; if (!sessionId) { @@ -1746,6 +1820,9 @@ export function heartbeatService(db: Db) { issueId ? `- Issue: ${issueId}` : "", `- Rotation reason: ${reason}`, latestTextSummary ? `- Last run summary: ${latestTextSummary}` : "", + input.continuationSummaryBody + ? `- Issue continuation summary: ${input.continuationSummaryBody.slice(0, 1_500)}` + : "", "Continue from the current task state. Rebuild only the minimum context you need.", ] .filter(Boolean) @@ -2170,6 +2247,136 @@ export function heartbeatService(db: Db) { .where(eq(agentWakeupRequests.id, wakeupRequestId)); } + async function addContinuationExhaustedCommentOnce(input: { + run: typeof heartbeatRuns.$inferSelect; + issueId: string; + comment: string; + }) { + const existing = await db + .select({ id: issueComments.id }) + .from(issueComments) + .where( + and( + eq(issueComments.companyId, input.run.companyId), + eq(issueComments.issueId, input.issueId), + eq(issueComments.createdByRunId, input.run.id), + sql`${issueComments.body} like 'Bounded liveness continuation exhausted%'`, + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null); + if (existing) return; + await issuesSvc.addComment(input.issueId, input.comment, { + agentId: input.run.agentId, + runId: input.run.id, + }); + } + + async function handleRunLivenessContinuation(run: typeof heartbeatRuns.$inferSelect) { + const livenessState = run.livenessState as RunLivenessState | null; + if (livenessState !== "plan_only" && livenessState !== "empty_response") return; + + const context = parseObject(run.contextSnapshot); + const issueId = readNonEmptyString(context.issueId); + if (!issueId) return; + + const [issue, agent] = await Promise.all([ + db + .select({ + id: issues.id, + companyId: issues.companyId, + identifier: issues.identifier, + title: issues.title, + status: issues.status, + assigneeAgentId: issues.assigneeAgentId, + executionState: issues.executionState, + projectId: issues.projectId, + }) + .from(issues) + .where(and(eq(issues.id, issueId), eq(issues.companyId, run.companyId))) + .then((rows) => rows[0] ?? null), + db + .select({ + id: agents.id, + companyId: agents.companyId, + status: agents.status, + }) + .from(agents) + .where(eq(agents.id, run.agentId)) + .then((rows) => rows[0] ?? null), + ]); + + const budgetBlock = + issue && agent + ? await budgets.getInvocationBlock(issue.companyId, agent.id, { + issueId: issue.id, + projectId: issue.projectId, + }) + : null; + + const nextAttempt = readContinuationAttempt(run.continuationAttempt) + 1; + const idempotencyKey = issue + ? buildRunLivenessContinuationIdempotencyKey({ + issueId: issue.id, + sourceRunId: run.id, + livenessState, + nextAttempt, + }) + : null; + const existingWake = idempotencyKey + ? await findExistingRunLivenessContinuationWake(db, { + companyId: run.companyId, + idempotencyKey, + }) + : null; + + const decision = decideRunLivenessContinuation({ + run, + issue, + agent, + livenessState, + livenessReason: run.livenessReason, + nextAction: run.nextAction, + budgetBlocked: Boolean(budgetBlock), + idempotentWakeExists: Boolean(existingWake), + }); + + if (decision.kind === "exhausted") { + await setRunStatus(run.id, run.status, { + livenessReason: `${run.livenessReason ?? "Run ended without concrete progress"}; continuation attempts exhausted`, + }); + await addContinuationExhaustedCommentOnce({ + run, + issueId, + comment: decision.comment, + }); + return; + } + + if (decision.kind !== "enqueue") return; + + const continuationRun = await enqueueWakeup(run.agentId, { + source: "automation", + triggerDetail: "system", + reason: RUN_LIVENESS_CONTINUATION_REASON, + payload: decision.payload, + contextSnapshot: decision.contextSnapshot, + idempotencyKey: decision.idempotencyKey, + requestedByActorType: "system", + requestedByActorId: "heartbeat", + }); + + if (continuationRun) { + await db + .update(heartbeatRuns) + .set({ + continuationAttempt: decision.nextAttempt, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, continuationRun.id)); + } + } + async function appendRunEvent( run: typeof heartbeatRuns.$inferSelect, seq: number, @@ -2298,6 +2505,47 @@ export function heartbeatService(db: Db) { .then((rows) => rows[0] ?? null); } + async function refreshContinuationSummaryForRun( + run: typeof heartbeatRuns.$inferSelect, + agent: typeof agents.$inferSelect, + ) { + const contextSnapshot = parseObject(run.contextSnapshot); + const issueId = readNonEmptyString(contextSnapshot.issueId); + if (!issueId) return null; + try { + return await refreshIssueContinuationSummary({ + db, + issueId, + run: { + id: run.id, + status: run.status, + error: run.error, + errorCode: run.errorCode, + resultJson: run.resultJson as Record | null, + stdoutExcerpt: run.stdoutExcerpt, + stderrExcerpt: run.stderrExcerpt, + finishedAt: run.finishedAt, + }, + agent: { + id: agent.id, + name: agent.name, + adapterType: agent.adapterType, + }, + }); + } catch (err) { + logger.warn( + { + err, + runId: run.id, + issueId, + agentId: agent.id, + }, + "failed to refresh issue continuation summary", + ); + return null; + } + } + async function enqueueMissingIssueCommentRetry( run: typeof heartbeatRuns.$inferSelect, agent: typeof agents.$inferSelect, @@ -2737,6 +2985,194 @@ export function heartbeatService(db: Db) { } } + function mergeRunStopMetadataForAgent( + agent: Pick, + outcome: "succeeded" | "failed" | "cancelled" | "timed_out", + options?: { + resultJson?: Record | null; + errorCode?: string | null; + errorMessage?: string | null; + }, + ) { + const stopMetadata = buildHeartbeatRunStopMetadata({ + adapterType: agent.adapterType, + adapterConfig: parseObject(agent.adapterConfig), + outcome, + errorCode: options?.errorCode ?? null, + errorMessage: options?.errorMessage ?? null, + }); + return mergeHeartbeatRunStopMetadata(options?.resultJson ?? null, stopMetadata); + } + + function countValue(value: unknown) { + const parsed = Number(value ?? 0); + return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0; + } + + function dateValue(value: unknown) { + 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; + } + return null; + } + + function latestDate(...values: unknown[]) { + let latest: Date | null = null; + for (const value of values) { + const parsed = dateValue(value); + if (!parsed) continue; + if (!latest || parsed.getTime() > latest.getTime()) latest = parsed; + } + return latest; + } + + async function buildRunLivenessInput( + run: typeof heartbeatRuns.$inferSelect, + resultJson: Record | null | undefined, + ): Promise { + const context = parseObject(run.contextSnapshot); + const contextIssueId = readNonEmptyString(context.issueId); + 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) + : 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), + ), + ) + : [{ count: 0, latestAt: null }]; + + 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}`, + ), + ) + : [{ 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), + ), + ) + : [{ count: 0, latestAt: null }]; + + const [workspaceOperationStats] = await db + .select({ + count: sql`count(*)::int`, + latestAt: sql`max(${workspaceOperations.startedAt})`, + }) + .from(workspaceOperations) + .where(and(eq(workspaceOperations.companyId, run.companyId), eq(workspaceOperations.heartbeatRunId, run.id))); + + const [activityStats] = await db + .select({ + count: sql`count(*)::int`, + latestAt: sql`max(${activityLog.createdAt})`, + }) + .from(activityLog) + .where(and(eq(activityLog.companyId, run.companyId), eq(activityLog.runId, run.id))); + + const [eventStats] = await db + .select({ + count: sql`count(*) filter (where ${heartbeatRunEvents.eventType} not in ('lifecycle', 'adapter.invoke', 'error'))::int`, + 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))); + + return { + runStatus: run.status, + issue, + resultJson: resultJson ?? run.resultJson ?? null, + stdoutExcerpt: run.stdoutExcerpt ?? null, + stderrExcerpt: run.stderrExcerpt ?? null, + error: run.error ?? null, + errorCode: run.errorCode ?? null, + continuationAttempt, + evidence: { + issueCommentsCreated: countValue(commentStats?.count), + documentRevisionsCreated: countValue(documentStats?.count), + planDocumentRevisionsCreated: countValue(documentStats?.planCount), + workProductsCreated: countValue(workProductStats?.count), + workspaceOperationsCreated: countValue(workspaceOperationStats?.count), + activityEventsCreated: countValue(activityStats?.count), + toolOrActionEventsCreated: countValue(eventStats?.count), + latestEvidenceAt: latestDate( + commentStats?.latestAt, + documentStats?.latestAt, + workProductStats?.latestAt, + workspaceOperationStats?.latestAt, + activityStats?.latestAt, + eventStats?.latestAt, + ), + }, + }; + } + + async function classifyAndPersistRunLiveness( + run: typeof heartbeatRuns.$inferSelect, + resultJson?: Record | null, + ) { + const classification = classifyRunLiveness(await buildRunLivenessInput(run, resultJson)); + return db + .update(heartbeatRuns) + .set({ + livenessState: classification.livenessState, + livenessReason: classification.livenessReason, + continuationAttempt: classification.continuationAttempt, + lastUsefulActionAt: classification.lastUsefulActionAt, + nextAction: classification.nextAction, + updatedAt: new Date(), + }) + .where(eq(heartbeatRuns.id, run.id)) + .returning() + .then((rows) => rows[0] ?? null); + } + async function reapOrphanedRuns(opts?: { staleThresholdMs?: number }) { const staleThresholdMs = opts?.staleThresholdMs ?? 0; const now = new Date(); @@ -2746,6 +3182,7 @@ export function heartbeatService(db: Db) { .select({ run: heartbeatRuns, adapterType: agents.adapterType, + adapterConfig: agents.adapterConfig, }) .from(heartbeatRuns) .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id)) @@ -2753,7 +3190,7 @@ export function heartbeatService(db: Db) { const reaped: string[] = []; - for (const { run, adapterType } of activeRuns) { + for (const { run, adapterType, adapterConfig } of activeRuns) { if (runningProcesses.has(run.id) || activeRunExecutions.has(run.id)) continue; // Apply staleness threshold to avoid false positives @@ -2803,6 +3240,15 @@ export function heartbeatService(db: Db) { error: shouldRetry ? `${baseMessage}; retrying once` : baseMessage, errorCode: "process_lost", finishedAt: now, + resultJson: mergeRunStopMetadataForAgent( + { adapterType, adapterConfig }, + "failed", + { + resultJson: parseObject(run.resultJson), + errorCode: "process_lost", + errorMessage: shouldRetry ? `${baseMessage}; retrying once` : baseMessage, + }, + ), }); await setWakeupStatus(run.wakeupRequestId, "failed", { finishedAt: now, @@ -2810,6 +3256,7 @@ export function heartbeatService(db: Db) { }); if (!finalizedRun) finalizedRun = await getRun(run.id); if (!finalizedRun) continue; + finalizedRun = await classifyAndPersistRunLiveness(finalizedRun, parseObject(finalizedRun.resultJson)) ?? finalizedRun; let retriedRun: typeof heartbeatRuns.$inferSelect | null = null; if (shouldRetry) { @@ -3340,10 +3787,24 @@ export function heartbeatService(db: Db) { executionWorkspacePreference: issueContext.executionWorkspacePreference, } : null; + const continuationSummary = issueRef + ? await getIssueContinuationSummaryDocument(db, issueRef.id) + : null; + if (continuationSummary) { + context.paperclipContinuationSummary = { + key: continuationSummary.key, + title: continuationSummary.title, + body: continuationSummary.body, + updatedAt: continuationSummary.updatedAt.toISOString(), + }; + } else { + delete context.paperclipContinuationSummary; + } const paperclipWakePayload = await buildPaperclipWakePayload({ db, companyId: agent.companyId, contextSnapshot: context, + continuationSummary, issueSummary: issueRef ? { id: issueRef.id, @@ -3656,6 +4117,7 @@ export function heartbeatService(db: Db) { agent, sessionId: previousSessionDisplayId ?? runtimeSessionIdForAdapter, issueId, + continuationSummaryBody: continuationSummary?.body ?? null, }); if (sessionCompaction.rotate) { context.paperclipSessionHandoffMarkdown = sessionCompaction.handoffMarkdown; @@ -3962,6 +4424,23 @@ export function heartbeatService(db: Db) { } else { outcome = "failed"; } + const runErrorMessage = + outcome === "cancelled" + ? (latestRun?.error ?? adapterResult.errorMessage ?? "Cancelled") + : outcome === "succeeded" + ? null + : redactCurrentUserText( + adapterResult.errorMessage ?? (outcome === "timed_out" ? "Timed out" : "Adapter failed"), + currentUserRedactionOptions, + ); + const runErrorCode = + outcome === "timed_out" + ? "timeout" + : outcome === "cancelled" + ? (latestRun?.errorCode ?? "cancelled") + : outcome === "failed" + ? (adapterResult.errorCode ?? "adapter_failed") + : null; let logSummary: { bytes: number; sha256?: string; compressed: boolean } | null = null; if (handle) { @@ -4004,27 +4483,18 @@ export function heartbeatService(db: Db) { : null; const persistedResultJson = mergeHeartbeatRunResultJson( - adapterResult.resultJson ?? null, + mergeRunStopMetadataForAgent(agent, outcome, { + resultJson: adapterResult.resultJson ?? null, + errorCode: runErrorCode, + errorMessage: runErrorMessage, + }), adapterResult.summary ?? null, ); - await setRunStatus(run.id, status, { + let persistedRun = await setRunStatus(run.id, status, { finishedAt: new Date(), - error: - outcome === "succeeded" - ? null - : redactCurrentUserText( - adapterResult.errorMessage ?? (outcome === "timed_out" ? "Timed out" : "Adapter failed"), - currentUserRedactionOptions, - ), - errorCode: - outcome === "timed_out" - ? "timeout" - : outcome === "cancelled" - ? "cancelled" - : outcome === "failed" - ? (adapterResult.errorCode ?? "adapter_failed") - : null, + error: runErrorMessage, + errorCode: runErrorCode, exitCode: adapterResult.exitCode, signal: adapterResult.signal, usageJson, @@ -4036,13 +4506,16 @@ export function heartbeatService(db: Db) { logSha256: logSummary?.sha256, logCompressed: logSummary?.compressed ?? false, }); + if (persistedRun) { + persistedRun = await classifyAndPersistRunLiveness(persistedRun, persistedResultJson) ?? persistedRun; + } await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded" ? "completed" : status, { finishedAt: new Date(), - error: adapterResult.errorMessage ?? null, + error: runErrorMessage, }); - const finalizedRun = await getRun(run.id); + const finalizedRun = persistedRun ?? (await getRun(run.id)); if (finalizedRun) { await appendRunEvent(finalizedRun, seq++, { eventType: "lifecycle", @@ -4054,13 +4527,15 @@ export function heartbeatService(db: Db) { exitCode: adapterResult.exitCode, }, }); + const livenessRun = finalizedRun; + await refreshContinuationSummaryForRun(livenessRun, agent); if (issueId && outcome === "succeeded") { try { - const existingRunComment = await findRunIssueComment(finalizedRun.id, finalizedRun.companyId, issueId); + const existingRunComment = await findRunIssueComment(livenessRun.id, livenessRun.companyId, issueId); if (!existingRunComment) { const issueComment = buildHeartbeatRunIssueComment(persistedResultJson); if (issueComment) { - await issuesSvc.addComment(issueId, issueComment, { agentId: agent.id, runId: finalizedRun.id }); + await issuesSvc.addComment(issueId, issueComment, { agentId: agent.id, runId: livenessRun.id }); } } } catch (err) { @@ -4070,8 +4545,9 @@ export function heartbeatService(db: Db) { ); } } - await finalizeIssueCommentPolicy(finalizedRun, agent); - await releaseIssueExecutionAndPromote(finalizedRun); + await finalizeIssueCommentPolicy(livenessRun, agent); + await releaseIssueExecutionAndPromote(livenessRun); + await handleRunLivenessContinuation(livenessRun); } if (finalizedRun) { @@ -4119,6 +4595,10 @@ export function heartbeatService(db: Db) { error: message, errorCode: "adapter_failed", finishedAt: new Date(), + resultJson: mergeRunStopMetadataForAgent(agent, "failed", { + errorCode: "adapter_failed", + errorMessage: message, + }), stdoutExcerpt, stderrExcerpt, logBytes: logSummary?.bytes, @@ -4137,10 +4617,12 @@ export function heartbeatService(db: Db) { level: "error", message, }); - await finalizeIssueCommentPolicy(failedRun, agent); - await releaseIssueExecutionAndPromote(failedRun); + const livenessRun = await classifyAndPersistRunLiveness(failedRun) ?? failedRun; + await refreshContinuationSummaryForRun(livenessRun, agent); + await finalizeIssueCommentPolicy(livenessRun, agent); + await releaseIssueExecutionAndPromote(livenessRun); - await updateRuntimeState(agent, failedRun, { + await updateRuntimeState(agent, livenessRun, { exitCode: null, signal: null, timedOut: false, @@ -4170,10 +4652,17 @@ export function heartbeatService(db: Db) { // The inner catch did not fire, so we must record the failure here. const message = outerErr instanceof Error ? outerErr.message : "Unknown setup failure"; logger.error({ err: outerErr, runId }, "heartbeat execution setup failed"); + const setupFailureAgent = await getAgent(run.agentId).catch(() => null); await setRunStatus(runId, "failed", { error: message, errorCode: "adapter_failed", finishedAt: new Date(), + ...(setupFailureAgent ? { + resultJson: mergeRunStopMetadataForAgent(setupFailureAgent, "failed", { + errorCode: "adapter_failed", + errorMessage: message, + }), + } : {}), }).catch(() => undefined); await setWakeupStatus(run.wakeupRequestId, "failed", { finishedAt: new Date(), @@ -4189,11 +4678,13 @@ export function heartbeatService(db: Db) { level: "error", message, }).catch(() => undefined); - const failedAgent = await getAgent(run.agentId).catch(() => null); + const livenessRun = await classifyAndPersistRunLiveness(failedRun).catch(() => failedRun); + const failedAgent = setupFailureAgent ?? await getAgent(run.agentId).catch(() => null); if (failedAgent) { - await finalizeIssueCommentPolicy(failedRun, failedAgent).catch(() => undefined); + await refreshContinuationSummaryForRun(livenessRun, failedAgent).catch(() => undefined); + await finalizeIssueCommentPolicy(livenessRun, failedAgent).catch(() => undefined); } - await releaseIssueExecutionAndPromote(failedRun).catch(() => undefined); + await releaseIssueExecutionAndPromote(livenessRun).catch(() => undefined); } // Ensure the agent is not left stuck in "running" if the inner catch handler's // DB calls threw (e.g. a transient DB error in finalizeAgentStatus). @@ -4363,6 +4854,9 @@ export function heartbeatService(db: Db) { const sessionBefore = readNonEmptyString(promotedContextSnapshot.resumeSessionDisplayId) ?? await resolveSessionBeforeForWakeup(deferredAgent, promotedTaskKey); + const promotedContinuationAttempt = readContinuationAttempt( + promotedContextSnapshot.livenessContinuationAttempt, + ); const now = new Date(); const newRun = await tx .insert(heartbeatRuns) @@ -4375,6 +4869,7 @@ export function heartbeatService(db: Db) { wakeupRequestId: deferred.id, contextSnapshot: promotedContextSnapshot, sessionIdBefore: sessionBefore, + continuationAttempt: promotedContinuationAttempt, }) .returning() .then((rows) => rows[0]); @@ -4473,6 +4968,7 @@ export function heartbeatService(db: Db) { const sessionBefore = explicitResumeSession?.sessionDisplayId ?? await resolveSessionBeforeForWakeup(agent, effectiveTaskKey); + const continuationAttempt = readContinuationAttempt(enrichedContextSnapshot.livenessContinuationAttempt); const writeSkippedRequest = async (skipReason: string) => { await db.insert(agentWakeupRequests).values({ @@ -4771,6 +5267,7 @@ export function heartbeatService(db: Db) { wakeupRequestId: wakeupRequest.id, contextSnapshot: enrichedContextSnapshot, sessionIdBefore: sessionBefore, + continuationAttempt, }) .returning() .then((rows) => rows[0]); @@ -4890,6 +5387,7 @@ export function heartbeatService(db: Db) { wakeupRequestId: wakeupRequest.id, contextSnapshot: enrichedContextSnapshot, sessionIdBefore: sessionBefore, + continuationAttempt, }) .returning() .then((rows) => rows[0]); @@ -5022,6 +5520,7 @@ export function heartbeatService(db: Db) { const run = await getRun(runId); if (!run) throw notFound("Heartbeat run not found"); if (run.status !== "running" && run.status !== "queued") return run; + const agent = await getAgent(run.agentId); const running = runningProcesses.get(run.id); if (running) { @@ -5041,6 +5540,13 @@ export function heartbeatService(db: Db) { finishedAt: new Date(), error: reason, errorCode: "cancelled", + ...(agent ? { + resultJson: mergeRunStopMetadataForAgent(agent, "cancelled", { + resultJson: parseObject(run.resultJson), + errorCode: "cancelled", + errorMessage: reason, + }), + } : {}), }); await setWakeupStatus(run.wakeupRequestId, "cancelled", { @@ -5065,6 +5571,7 @@ export function heartbeatService(db: Db) { } async function cancelActiveForAgentInternal(agentId: string, reason = "Cancelled due to agent pause") { + const agent = await getAgent(agentId); const runs = await db .select() .from(heartbeatRuns) @@ -5075,6 +5582,13 @@ export function heartbeatService(db: Db) { finishedAt: new Date(), error: reason, errorCode: "cancelled", + ...(agent ? { + resultJson: mergeRunStopMetadataForAgent(agent, "cancelled", { + resultJson: parseObject(run.resultJson), + errorCode: "cancelled", + errorMessage: reason, + }), + } : {}), }); await setWakeupStatus(run.wakeupRequestId, "cancelled", { diff --git a/server/src/services/index.ts b/server/src/services/index.ts index c279dd13be..e999a9cc78 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -5,6 +5,12 @@ export { agentService, deduplicateAgentName } from "./agents.js"; export { agentInstructionsService, syncInstructionsBundleConfigFromFilePath } from "./agent-instructions.js"; export { assetService } from "./assets.js"; export { documentService, extractLegacyPlanBody } from "./documents.js"; +export { + ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + buildContinuationSummaryMarkdown, + getIssueContinuationSummaryDocument, + refreshIssueContinuationSummary, +} from "./issue-continuation-summary.js"; export { projectService } from "./projects.js"; export { issueService, type IssueFilters } from "./issues.js"; export { issueApprovalService } from "./issue-approvals.js"; diff --git a/server/src/services/issue-continuation-summary.ts b/server/src/services/issue-continuation-summary.ts new file mode 100644 index 0000000000..422e3a5b15 --- /dev/null +++ b/server/src/services/issue-continuation-summary.ts @@ -0,0 +1,269 @@ +import { and, eq } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { documents, issueDocuments, issues } from "@paperclipai/db"; +import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared"; +import { documentService } from "./documents.js"; + +export { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY }; +export const ISSUE_CONTINUATION_SUMMARY_TITLE = "Continuation Summary"; +export const ISSUE_CONTINUATION_SUMMARY_MAX_BODY_CHARS = 8_000; +const SUMMARY_SECTION_MAX_CHARS = 1_200; +const PATH_CANDIDATE_RE = /(?:^|[\s`"'(])((?:server|ui|packages|doc|scripts|\.github)\/[A-Za-z0-9._/-]+)/g; + +type IssueSummaryInput = { + id: string; + identifier: string | null; + title: string; + description: string | null; + status: string; + priority: string; +}; + +type RunSummaryInput = { + id: string; + status: string; + error: string | null; + errorCode?: string | null; + resultJson?: Record | null; + stdoutExcerpt?: string | null; + stderrExcerpt?: string | null; + finishedAt?: Date | null; +}; + +type AgentSummaryInput = { + id: string; + name: string; + adapterType: string | null; +}; + +export type IssueContinuationSummaryDocument = { + key: typeof ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY; + title: string | null; + body: string; + latestRevisionId: string | null; + latestRevisionNumber: number; + updatedAt: Date; +}; + +function truncateText(value: string, maxChars: number) { + const trimmed = value.trim(); + if (trimmed.length <= maxChars) return trimmed; + return `${trimmed.slice(0, Math.max(0, maxChars - 20)).trimEnd()}\n[truncated]`; +} + +function asNonEmptyString(value: unknown) { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +function readResultSummary(resultJson: Record | null | undefined) { + if (!resultJson || typeof resultJson !== "object" || Array.isArray(resultJson)) return null; + return ( + asNonEmptyString(resultJson.summary) ?? + asNonEmptyString(resultJson.result) ?? + asNonEmptyString(resultJson.message) ?? + asNonEmptyString(resultJson.error) ?? + null + ); +} + +function extractMarkdownSection(markdown: string | null | undefined, heading: string) { + if (!markdown) return null; + const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const re = new RegExp(`^##\\s+${escaped}\\s*$([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, "im"); + const match = re.exec(markdown); + const section = match?.[1]?.trim(); + return section ? truncateText(section, SUMMARY_SECTION_MAX_CHARS) : null; +} + +function extractPathCandidates(...texts: Array) { + const seen = new Set(); + for (const text of texts) { + if (!text) continue; + for (const match of text.matchAll(PATH_CANDIDATE_RE)) { + const path = match[1]?.replace(/[),.;:]+$/, ""); + if (path) seen.add(path); + if (seen.size >= 12) break; + } + if (seen.size >= 12) break; + } + return [...seen]; +} + +function inferMode(issue: IssueSummaryInput, run: RunSummaryInput) { + if (issue.status === "done" || issue.status === "in_review") return "review"; + if (run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") return "implementation"; + if (issue.status === "backlog" || issue.status === "todo") return "plan"; + return "implementation"; +} + +function inferNextAction(issue: IssueSummaryInput, run: RunSummaryInput, previousNextAction: string | null) { + if (issue.status === "done") return "Review the completed issue output and close any remaining follow-up comments."; + if (issue.status === "in_review") return "Wait for reviewer feedback or approval before continuing executor work."; + if (run.status === "failed" || run.status === "timed_out") { + return "Inspect the failed run, fix the cause, and resume from the most recent concrete action above."; + } + if (run.status === "cancelled") return "Confirm the cancellation reason before starting another run."; + return previousNextAction ?? "Resume implementation from the acceptance criteria, latest comments, and this summary."; +} + +function bulletList(items: string[], empty: string) { + if (items.length === 0) return `- ${empty}`; + return items.map((item) => `- ${item}`).join("\n"); +} + +function extractPreviousNextAction(previousBody: string | null | undefined) { + const section = extractMarkdownSection(previousBody, "Next Action"); + if (!section) return null; + return section + .split(/\r?\n/) + .map((line) => line.replace(/^[-*]\s+/, "").trim()) + .find(Boolean) ?? null; +} + +export function buildContinuationSummaryMarkdown(input: { + issue: IssueSummaryInput; + run: RunSummaryInput; + agent: AgentSummaryInput; + previousSummaryBody?: string | null; +}) { + const { issue, run, agent } = input; + const resultSummary = readResultSummary(run.resultJson); + const recentActions = [ + `Run \`${run.id}\` finished with status \`${run.status}\`${run.finishedAt ? ` at ${run.finishedAt.toISOString()}` : ""}.`, + resultSummary ? truncateText(resultSummary, SUMMARY_SECTION_MAX_CHARS) : "No adapter-provided result summary was captured for this run.", + ]; + if (run.error) { + recentActions.push(`Latest run error${run.errorCode ? ` (${run.errorCode})` : ""}: ${truncateText(run.error, 500)}`); + } + + const paths = extractPathCandidates(resultSummary, run.stdoutExcerpt, run.stderrExcerpt, input.previousSummaryBody); + const objective = extractMarkdownSection(issue.description, "Objective") ?? issue.description?.trim() ?? "No objective captured."; + const acceptanceCriteria = extractMarkdownSection(issue.description, "Acceptance Criteria") ?? "No explicit acceptance criteria captured."; + const mode = inferMode(issue, run); + const nextAction = inferNextAction(issue, run, extractPreviousNextAction(input.previousSummaryBody)); + + const body = [ + "# Continuation Summary", + "", + `- Issue: ${issue.identifier ?? issue.id} — ${issue.title}`, + `- Status: ${issue.status}`, + `- Priority: ${issue.priority}`, + `- Current mode: ${mode}`, + `- Last updated by run: ${run.id}`, + `- Agent: ${agent.name} (${agent.adapterType ?? "unknown"})`, + "", + "## Objective", + "", + truncateText(objective, SUMMARY_SECTION_MAX_CHARS), + "", + "## Acceptance Criteria", + "", + acceptanceCriteria, + "", + "## Recent Concrete Actions", + "", + bulletList(recentActions, "No recent actions captured."), + "", + "## Files / Routes Touched", + "", + bulletList(paths.map((path) => `\`${path}\``), "No file or route paths were detected in the captured run summary."), + "", + "## Commands Run", + "", + bulletList( + [ + `Heartbeat run \`${run.id}\` invoked adapter \`${agent.adapterType ?? "unknown"}\`.`, + "Detailed shell/tool commands remain in the run log and transcript.", + ], + "No command metadata captured.", + ), + "", + "## Blockers / Decisions", + "", + bulletList( + run.error + ? [`Latest run ended with \`${run.status}\`; inspect the error before continuing.`] + : ["No new blocker was recorded by the latest run."], + "No blockers or decisions captured.", + ), + "", + "## Next Action", + "", + `- ${nextAction}`, + ].join("\n"); + + return truncateText(body, ISSUE_CONTINUATION_SUMMARY_MAX_BODY_CHARS); +} + +export async function getIssueContinuationSummaryDocument( + db: Db, + issueId: string, +): Promise { + const row = await db + .select({ + key: issueDocuments.key, + title: documents.title, + body: documents.latestBody, + latestRevisionId: documents.latestRevisionId, + latestRevisionNumber: documents.latestRevisionNumber, + updatedAt: documents.updatedAt, + }) + .from(issueDocuments) + .innerJoin(documents, eq(issueDocuments.documentId, documents.id)) + .where(and(eq(issueDocuments.issueId, issueId), eq(issueDocuments.key, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY))) + .then((rows) => rows[0] ?? null); + + if (!row) return null; + return { + key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + title: row.title, + body: row.body, + latestRevisionId: row.latestRevisionId, + latestRevisionNumber: row.latestRevisionNumber, + updatedAt: row.updatedAt, + }; +} + +export async function refreshIssueContinuationSummary(input: { + db: Db; + issueId: string; + run: RunSummaryInput; + agent: AgentSummaryInput; +}) { + const { db, issueId, run, agent } = input; + const [issue, existing] = await Promise.all([ + db + .select({ + id: issues.id, + identifier: issues.identifier, + title: issues.title, + description: issues.description, + status: issues.status, + priority: issues.priority, + }) + .from(issues) + .where(eq(issues.id, issueId)) + .then((rows) => rows[0] ?? null), + getIssueContinuationSummaryDocument(db, issueId), + ]); + + if (!issue) return null; + const body = buildContinuationSummaryMarkdown({ + issue, + run, + agent, + previousSummaryBody: existing?.body ?? null, + }); + const result = await documentService(db).upsertIssueDocument({ + issueId, + key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + title: ISSUE_CONTINUATION_SUMMARY_TITLE, + format: "markdown", + body, + baseRevisionId: existing?.latestRevisionId ?? null, + changeSummary: `Refresh continuation summary after run ${run.id}`, + createdByAgentId: agent.id, + createdByRunId: run.id, + }); + return result.document; +} diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 5d045e47ae..75133b1b15 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -38,6 +38,9 @@ import { getDefaultCompanyGoal } from "./goals.js"; const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"]; const MAX_ISSUE_COMMENT_PAGE_LIMIT = 500; +export const MAX_CHILD_ISSUES_CREATED_BY_HELPER = 25; +const MAX_CHILD_COMPLETION_SUMMARIES = 20; +const CHILD_COMPLETION_SUMMARY_BODY_MAX_CHARS = 500; function assertTransition(from: string, to: string) { if (from === to) return; @@ -121,10 +124,27 @@ type IssueCreateInput = Omit & { blockedByIssueIds?: string[]; inheritExecutionWorkspaceFromIssueId?: string | null; }; +type IssueChildCreateInput = IssueCreateInput & { + acceptanceCriteria?: string[]; + blockParentUntilDone?: boolean; + actorAgentId?: string | null; + actorUserId?: string | null; +}; type IssueRelationSummaryMap = { blockedBy: IssueRelationIssueSummary[]; blocks: IssueRelationIssueSummary[]; }; +export type ChildIssueCompletionSummary = { + id: string; + identifier: string | null; + title: string; + status: string; + priority: string; + assigneeAgentId: string | null; + assigneeUserId: string | null; + updatedAt: Date; + summary: string | null; +}; function sameRunLock(checkoutRunId: string | null, actorRunId: string | null) { if (actorRunId) return checkoutRunId === actorRunId; @@ -138,6 +158,20 @@ function escapeLikePattern(value: string): string { return value.replace(/[\\%_]/g, "\\$&"); } +function truncateInlineSummary(value: string | null | undefined, maxChars = CHILD_COMPLETION_SUMMARY_BODY_MAX_CHARS) { + const normalized = value?.trim(); + if (!normalized) return null; + return normalized.length > maxChars ? `${normalized.slice(0, Math.max(0, maxChars - 15)).trimEnd()} [truncated]` : normalized; +} + +function appendAcceptanceCriteriaToDescription(description: string | null | undefined, acceptanceCriteria: string[] | undefined) { + const criteria = (acceptanceCriteria ?? []).map((item) => item.trim()).filter(Boolean); + if (criteria.length === 0) return description ?? null; + const base = description?.trim() ?? ""; + const criteriaMarkdown = ["## Acceptance Criteria", "", ...criteria.map((item) => `- ${item}`)].join("\n"); + return base ? `${base}\n\n${criteriaMarkdown}` : criteriaMarkdown; +} + async function getProjectDefaultGoalId( db: ProjectGoalReader, companyId: string, @@ -1406,18 +1440,110 @@ export function issueService(db: Db) { } const children = await db - .select({ id: issues.id, status: issues.status }) + .select({ + id: issues.id, + identifier: issues.identifier, + title: issues.title, + status: issues.status, + priority: issues.priority, + assigneeAgentId: issues.assigneeAgentId, + assigneeUserId: issues.assigneeUserId, + updatedAt: issues.updatedAt, + }) .from(issues) - .where(and(eq(issues.companyId, parent.companyId), eq(issues.parentId, parentIssueId))); + .where(and(eq(issues.companyId, parent.companyId), eq(issues.parentId, parentIssueId))) + .orderBy(asc(issues.issueNumber), asc(issues.createdAt)); if (children.length === 0) return null; if (!children.every((child) => child.status === "done" || child.status === "cancelled")) { return null; } + const childIdsForSummaries = children.slice(0, MAX_CHILD_COMPLETION_SUMMARIES).map((child) => child.id); + const commentRows = childIdsForSummaries.length > 0 + ? await db + .select({ + issueId: issueComments.issueId, + body: issueComments.body, + createdAt: issueComments.createdAt, + }) + .from(issueComments) + .where(and(eq(issueComments.companyId, parent.companyId), inArray(issueComments.issueId, childIdsForSummaries))) + .orderBy(desc(issueComments.createdAt), desc(issueComments.id)) + : []; + const latestCommentByIssueId = new Map(); + for (const comment of commentRows) { + if (!latestCommentByIssueId.has(comment.issueId)) { + latestCommentByIssueId.set(comment.issueId, comment.body); + } + } + const childIssueSummaries: ChildIssueCompletionSummary[] = children + .slice(0, MAX_CHILD_COMPLETION_SUMMARIES) + .map((child) => ({ + ...child, + summary: truncateInlineSummary(latestCommentByIssueId.get(child.id)), + })); + return { id: parent.id, assigneeAgentId: parent.assigneeAgentId, childIssueIds: children.map((child) => child.id), + childIssueSummaries, + childIssueSummaryTruncated: children.length > childIssueSummaries.length, + }; + }, + + createChild: async ( + parentIssueId: string, + data: IssueChildCreateInput, + ) => { + const parent = await db + .select() + .from(issues) + .where(eq(issues.id, parentIssueId)) + .then((rows) => rows[0] ?? null); + if (!parent) throw notFound("Parent issue not found"); + + const [{ childCount }] = await db + .select({ childCount: sql`count(*)::int` }) + .from(issues) + .where(and(eq(issues.companyId, parent.companyId), eq(issues.parentId, parent.id))); + if (childCount >= MAX_CHILD_ISSUES_CREATED_BY_HELPER) { + throw unprocessable(`Parent issue already has the maximum ${MAX_CHILD_ISSUES_CREATED_BY_HELPER} child issues for this helper`); + } + + const { + acceptanceCriteria, + blockParentUntilDone, + actorAgentId, + actorUserId, + ...issueData + } = data; + const child = await issueService(db).create(parent.companyId, { + ...issueData, + parentId: parent.id, + projectId: issueData.projectId ?? parent.projectId, + goalId: issueData.goalId ?? parent.goalId, + requestDepth: Math.max(parent.requestDepth + 1, issueData.requestDepth ?? 0), + description: appendAcceptanceCriteriaToDescription(issueData.description, acceptanceCriteria), + inheritExecutionWorkspaceFromIssueId: parent.id, + }); + + if (blockParentUntilDone) { + const existingBlockers = await db + .select({ blockerIssueId: issueRelations.issueId }) + .from(issueRelations) + .where(and(eq(issueRelations.companyId, parent.companyId), eq(issueRelations.relatedIssueId, parent.id), eq(issueRelations.type, "blocks"))); + await syncBlockedByIssueIds( + parent.id, + parent.companyId, + [...new Set([...existingBlockers.map((row) => row.blockerIssueId), child.id])], + { agentId: actorAgentId ?? null, userId: actorUserId ?? null }, + ); + } + + return { + issue: child, + parentBlockerAdded: Boolean(blockParentUntilDone), }; }, diff --git a/server/src/services/run-continuations.ts b/server/src/services/run-continuations.ts new file mode 100644 index 0000000000..5269ad5a03 --- /dev/null +++ b/server/src/services/run-continuations.ts @@ -0,0 +1,188 @@ +import { and, eq, inArray } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { agentWakeupRequests, agents, heartbeatRuns, issues } from "@paperclipai/db"; +import type { RunLivenessState } from "@paperclipai/shared"; + +export const RUN_LIVENESS_CONTINUATION_REASON = "run_liveness_continuation"; +export const DEFAULT_MAX_LIVENESS_CONTINUATION_ATTEMPTS = 2; + +const ACTIONABLE_LIVENESS_STATES = new Set(["plan_only", "empty_response"]); +const CONTINUATION_ACTIVE_ISSUE_STATUSES = new Set(["todo", "in_progress"]); +// A prior adapter error should not permanently suppress bounded liveness +// continuations; the max-attempt/idempotency guards prevent unbounded retries. +const CONTINUATION_AGENT_STATUSES = new Set(["active", "idle", "running", "error"]); +const IDEMPOTENT_WAKE_STATUSES = ["queued", "deferred_issue_execution", "completed"]; + +type HeartbeatRunRow = typeof heartbeatRuns.$inferSelect; +type IssueRow = Pick< + typeof issues.$inferSelect, + "id" | "companyId" | "identifier" | "title" | "status" | "assigneeAgentId" | "executionState" | "projectId" +>; +type AgentRow = Pick; + +export type RunContinuationDecision = + | { + kind: "enqueue"; + nextAttempt: number; + idempotencyKey: string; + payload: Record; + contextSnapshot: Record; + } + | { + kind: "exhausted"; + attempt: number; + maxAttempts: number; + comment: string; + } + | { + kind: "skip"; + reason: string; + }; + +export function readContinuationAttempt(value: unknown): number { + const numeric = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10); + return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0; +} + +export function buildRunLivenessContinuationIdempotencyKey(input: { + issueId: string; + sourceRunId: string; + livenessState: RunLivenessState; + nextAttempt: number; +}) { + return [ + "run_liveness_continuation", + input.issueId, + input.sourceRunId, + input.livenessState, + String(input.nextAttempt), + ].join(":"); +} + +export async function findExistingRunLivenessContinuationWake( + db: Db, + input: { + companyId: string; + idempotencyKey: string; + }, +) { + return db + .select({ id: agentWakeupRequests.id, status: agentWakeupRequests.status }) + .from(agentWakeupRequests) + .where( + and( + eq(agentWakeupRequests.companyId, input.companyId), + eq(agentWakeupRequests.idempotencyKey, input.idempotencyKey), + inArray(agentWakeupRequests.status, IDEMPOTENT_WAKE_STATUSES), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null); +} + +export function decideRunLivenessContinuation(input: { + run: HeartbeatRunRow; + issue: IssueRow | null; + agent: AgentRow | null; + livenessState: RunLivenessState | null; + livenessReason: string | null; + nextAction: string | null; + budgetBlocked: boolean; + idempotentWakeExists: boolean; + maxAttempts?: number; +}): RunContinuationDecision { + const { + run, + issue, + agent, + livenessState, + livenessReason, + nextAction, + budgetBlocked, + idempotentWakeExists, + } = input; + const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_LIVENESS_CONTINUATION_ATTEMPTS; + + if (!livenessState || !ACTIONABLE_LIVENESS_STATES.has(livenessState)) { + return { kind: "skip", reason: "liveness state is not actionable for continuation" }; + } + if (!issue) return { kind: "skip", reason: "issue not found" }; + if (!agent) return { kind: "skip", reason: "agent not found" }; + if (issue.companyId !== run.companyId || agent.companyId !== run.companyId) { + return { kind: "skip", reason: "company scope mismatch" }; + } + if (issue.assigneeAgentId !== run.agentId) { + return { kind: "skip", reason: "issue is no longer assigned to the source run agent" }; + } + if (!CONTINUATION_ACTIVE_ISSUE_STATUSES.has(issue.status)) { + return { kind: "skip", reason: `issue status ${issue.status} is not continuable` }; + } + if (issue.executionState) { + return { kind: "skip", reason: "issue is blocked by execution policy state" }; + } + if (!CONTINUATION_AGENT_STATUSES.has(agent.status)) { + return { kind: "skip", reason: `agent status ${agent.status} is not invokable` }; + } + if (budgetBlocked) { + return { kind: "skip", reason: "budget hard stop blocks continuation" }; + } + + const currentAttempt = readContinuationAttempt(run.continuationAttempt); + if (currentAttempt >= maxAttempts) { + return { + kind: "exhausted", + attempt: currentAttempt, + maxAttempts, + comment: [ + "Bounded liveness continuation exhausted", + "", + `- Last liveness state: \`${livenessState}\``, + `- Attempts used: ${currentAttempt}/${maxAttempts}`, + `- Reason: ${livenessReason ?? "Run ended without concrete progress"}`, + "- Next action: a human or manager should inspect the run and either clarify the task, mark it blocked, or assign a concrete follow-up.", + ].join("\n"), + }; + } + + const nextAttempt = currentAttempt + 1; + const idempotencyKey = buildRunLivenessContinuationIdempotencyKey({ + issueId: issue.id, + sourceRunId: run.id, + livenessState, + nextAttempt, + }); + if (idempotentWakeExists) { + return { kind: "skip", reason: "continuation wake already exists for this source run and attempt" }; + } + + const payload = { + issueId: issue.id, + sourceRunId: run.id, + livenessState, + livenessReason, + continuationAttempt: nextAttempt, + maxContinuationAttempts: maxAttempts, + instruction: + nextAction ?? + "The previous run ended without concrete progress. Take the first concrete action now or mark the issue blocked with a specific unblock request.", + }; + + return { + kind: "enqueue", + nextAttempt, + idempotencyKey, + payload, + contextSnapshot: { + issueId: issue.id, + taskId: issue.id, + taskKey: issue.id, + wakeReason: RUN_LIVENESS_CONTINUATION_REASON, + livenessContinuationAttempt: nextAttempt, + livenessContinuationMaxAttempts: maxAttempts, + livenessContinuationSourceRunId: run.id, + livenessContinuationState: livenessState, + livenessContinuationReason: livenessReason, + livenessContinuationInstruction: payload.instruction, + }, + }; +} diff --git a/server/src/services/run-liveness.ts b/server/src/services/run-liveness.ts new file mode 100644 index 0000000000..75f82909b1 --- /dev/null +++ b/server/src/services/run-liveness.ts @@ -0,0 +1,227 @@ +import type { HeartbeatRunStatus, IssueStatus, RunLivenessState } from "@paperclipai/shared"; + +export interface RunLivenessIssueInput { + status: IssueStatus | string; + title: string; + description: string | null; +} + +export interface RunLivenessEvidenceInput { + issueCommentsCreated: number; + documentRevisionsCreated: number; + planDocumentRevisionsCreated: number; + workProductsCreated: number; + workspaceOperationsCreated: number; + activityEventsCreated: number; + toolOrActionEventsCreated: number; + latestEvidenceAt: Date | null; +} + +export interface RunLivenessClassificationInput { + runStatus: HeartbeatRunStatus | string; + issue: RunLivenessIssueInput | null; + resultJson?: Record | null; + stdoutExcerpt?: string | null; + stderrExcerpt?: string | null; + error?: string | null; + errorCode?: string | null; + continuationAttempt?: number | null; + evidence?: Partial | null; +} + +export interface RunLivenessClassification { + livenessState: RunLivenessState; + livenessReason: string; + continuationAttempt: number; + lastUsefulActionAt: Date | null; + nextAction: string | null; +} + +const DEFAULT_EVIDENCE: RunLivenessEvidenceInput = { + issueCommentsCreated: 0, + documentRevisionsCreated: 0, + planDocumentRevisionsCreated: 0, + workProductsCreated: 0, + workspaceOperationsCreated: 0, + activityEventsCreated: 0, + toolOrActionEventsCreated: 0, + latestEvidenceAt: null, +}; + +const PLANNING_ONLY_RE = + /\b(?:i(?:'ll| will| am going to|'m going to)|let me|i need to|next(?:,| i will| i'll)?|my next step is|the next step is)\s+(?:first\s+)?(?:inspect|check|review|look|investigate|analy[sz]e|open|read|start|begin|work on|implement|fix|test|update|create|add)\b/i; +const NEXT_STEPS_RE = /^\s*(?:next steps?|plan)\s*:/im; +const BLOCKER_RE = + /\b(?:blocked|can't proceed|cannot proceed|unable to proceed|waiting on|need(?:s|ed)? .{0,80}\b(?:approval|access|credential|credentials|secret|api key|token|input|clarification)|requires? .{0,80}\b(?:approval|access|credential|credentials|secret|api key|token|input|clarification))\b/i; +const NEGATED_BLOCKER_RE = /\b(?:not blocked|no blocker|no blockers|unblocked)\b/i; +const PLAN_TASK_TITLE_RE = /\b(?:plan|planning|analysis|investigation|research|report|proposal|design doc|write-?up)\b/i; +const PLAN_TASK_DESCRIPTION_RE = + /\b(?:create|write|produce|draft|update|revise|prepare)\s+(?:a\s+|the\s+)?(?:plan|analysis|investigation|research report|report|proposal|design doc|write-?up)\b/i; + +function compactReason(reason: string) { + return reason.length <= 500 ? reason : `${reason.slice(0, 497)}...`; +} + +function normalizeCount(value: unknown) { + return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; +} + +function normalizeContinuationAttempt(value: unknown) { + return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; +} + +function readText(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function resultText(resultJson: Record | null | undefined) { + if (!resultJson) return ""; + return [ + readText(resultJson.summary), + readText(resultJson.result), + readText(resultJson.message), + readText(resultJson.stdout), + readText(resultJson.stderr), + ] + .filter((value): value is string => Boolean(value)) + .join("\n"); +} + +function combinedOutput(input: RunLivenessClassificationInput) { + return [ + resultText(input.resultJson), + readText(input.stdoutExcerpt), + readText(input.stderrExcerpt), + readText(input.error), + ] + .filter((value): value is string => Boolean(value)) + .join("\n") + .trim(); +} + +export function hasUsefulOutput(input: RunLivenessClassificationInput) { + return combinedOutput(input).length > 0; +} + +export function declaredBlocker(input: RunLivenessClassificationInput) { + if (input.issue?.status === "blocked") return true; + const text = combinedOutput(input); + if (!text || NEGATED_BLOCKER_RE.test(text)) return false; + return BLOCKER_RE.test(text); +} + +export function looksLikePlanningOnly(input: RunLivenessClassificationInput) { + const text = combinedOutput(input); + if (!text) return false; + return PLANNING_ONLY_RE.test(text) || NEXT_STEPS_RE.test(text); +} + +export function isPlanningOrDocumentTask(issue: RunLivenessIssueInput | null | undefined) { + if (!issue) return false; + if (PLAN_TASK_TITLE_RE.test(issue.title)) return true; + return PLAN_TASK_DESCRIPTION_RE.test(issue.description ?? ""); +} + +function normalizeEvidence(evidence: Partial | null | undefined): RunLivenessEvidenceInput { + return { + issueCommentsCreated: normalizeCount(evidence?.issueCommentsCreated), + documentRevisionsCreated: normalizeCount(evidence?.documentRevisionsCreated), + planDocumentRevisionsCreated: normalizeCount(evidence?.planDocumentRevisionsCreated), + workProductsCreated: normalizeCount(evidence?.workProductsCreated), + workspaceOperationsCreated: normalizeCount(evidence?.workspaceOperationsCreated), + activityEventsCreated: normalizeCount(evidence?.activityEventsCreated), + toolOrActionEventsCreated: normalizeCount(evidence?.toolOrActionEventsCreated), + latestEvidenceAt: evidence?.latestEvidenceAt instanceof Date ? evidence.latestEvidenceAt : null, + }; +} + +export function hasConcreteActionEvidence(evidence: Partial | null | undefined) { + const normalized = normalizeEvidence(evidence); + // Workspace creation is setup evidence, not task progress by itself. It can + // appear in reasons alongside durable activity, but it must not prevent a + // planning-only or empty run from receiving a bounded continuation. + return ( + normalized.issueCommentsCreated + + normalized.documentRevisionsCreated + + normalized.workProductsCreated + + normalized.activityEventsCreated + + normalized.toolOrActionEventsCreated > + 0 + ); +} + +function evidenceReason(evidence: RunLivenessEvidenceInput) { + const parts: string[] = []; + if (evidence.issueCommentsCreated > 0) parts.push(`${evidence.issueCommentsCreated} issue comment(s)`); + if (evidence.documentRevisionsCreated > 0) parts.push(`${evidence.documentRevisionsCreated} document revision(s)`); + if (evidence.workProductsCreated > 0) parts.push(`${evidence.workProductsCreated} work product(s)`); + if (evidence.workspaceOperationsCreated > 0) parts.push(`${evidence.workspaceOperationsCreated} workspace operation(s)`); + if (evidence.activityEventsCreated > 0) parts.push(`${evidence.activityEventsCreated} activity event(s)`); + if (evidence.toolOrActionEventsCreated > 0) parts.push(`${evidence.toolOrActionEventsCreated} tool/action event(s)`); + return parts.join(", "); +} + +function extractNextAction(input: RunLivenessClassificationInput) { + const text = combinedOutput(input); + if (!text) return null; + const line = text + .split(/\r?\n/) + .map((entry) => entry.trim()) + .find((entry) => PLANNING_ONLY_RE.test(entry) || /^next(?: steps?| action)?\s*:/i.test(entry)); + if (!line) return null; + return line.length <= 500 ? line : `${line.slice(0, 497)}...`; +} + +export function classifyRunLiveness(input: RunLivenessClassificationInput): RunLivenessClassification { + const evidence = normalizeEvidence(input.evidence); + const continuationAttempt = normalizeContinuationAttempt(input.continuationAttempt); + const issueStatus = input.issue?.status ?? null; + const usefulOutput = hasUsefulOutput(input); + const concreteEvidence = hasConcreteActionEvidence(evidence); + const planExempt = isPlanningOrDocumentTask(input.issue) || evidence.planDocumentRevisionsCreated > 0; + const lastUsefulActionAt = concreteEvidence ? evidence.latestEvidenceAt : null; + + const output = (state: RunLivenessState, reason: string, nextAction: string | null = null): RunLivenessClassification => ({ + livenessState: state, + livenessReason: compactReason(reason), + continuationAttempt, + lastUsefulActionAt: state === "advanced" || state === "completed" || state === "blocked" ? lastUsefulActionAt : null, + nextAction, + }); + + if (input.runStatus !== "succeeded") { + return output("failed", input.errorCode ? `Run ended with ${input.runStatus} (${input.errorCode})` : `Run ended with ${input.runStatus}`); + } + + if (issueStatus === "done" || issueStatus === "cancelled") { + return output("completed", `Issue is ${issueStatus}`); + } + + if (declaredBlocker(input)) { + return output("blocked", issueStatus === "blocked" ? "Issue status is blocked" : "Run output declared a concrete blocker", extractNextAction(input)); + } + + if (!usefulOutput && !concreteEvidence) { + return output("empty_response", "Run succeeded without useful output or concrete action evidence"); + } + + if (concreteEvidence) { + return output("advanced", `Run produced concrete action evidence: ${evidenceReason(evidence)}`); + } + + if (planExempt && usefulOutput) { + return output("advanced", "Planning/document task produced useful output and is exempt from plan-only classification"); + } + + if (looksLikePlanningOnly(input)) { + return output("plan_only", "Run described future work without concrete action evidence", extractNextAction(input)); + } + + if (usefulOutput) { + return output("needs_followup", "Run produced useful output but no concrete action evidence", extractNextAction(input)); + } + + return output("empty_response", "Run succeeded without useful output"); +} diff --git a/skills/paperclip-create-agent/SKILL.md b/skills/paperclip-create-agent/SKILL.md index e3178d53ae..878b255bbf 100644 --- a/skills/paperclip-create-agent/SKILL.md +++ b/skills/paperclip-create-agent/SKILL.md @@ -65,7 +65,7 @@ curl -sS "$PAPERCLIP_API_URL/llms/agent-icons.txt" \ - adapter and runtime config aligned to this environment - leave timer heartbeats off by default; only set `runtimeConfig.heartbeat.enabled=true` with an `intervalSec` when the role genuinely needs scheduled recurring work or the user explicitly asked for it - capabilities -- run prompt in adapter config (`promptTemplate` where applicable) +- run prompt in adapter config (`promptTemplate` where applicable). For coding or execution agents, include the Paperclip execution contract: start actionable work in the same heartbeat; do not stop at a plan unless planning was requested; leave durable progress with a clear next action; use child issues for long or parallel delegated work instead of polling; mark blocked work with owner/action; respect budget, pause/cancel, approval gates, and company boundaries. - source issue linkage (`sourceIssueId` or `sourceIssueIds`) when this hire came from an issue 7. Submit hire request. diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index 7b4a1461b9..7d127f569d 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -105,6 +105,14 @@ If `currentParticipant` does **not** match you, do not try to advance the stage. **Step 7 — Do the work.** Use your tools and capabilities. +Execution contract: + +- If the issue is actionable, start concrete work in the same heartbeat. Do not stop at a plan unless the issue specifically asks for planning. +- Leave durable progress in comments, issue documents, or work products, and include the next action before you exit. +- Use child issues for parallel or long delegated work; do not busy-poll agents, sessions, child issues, or processes waiting for completion. +- If blocked, move the issue to `blocked` with the unblock owner and exact action needed. +- Respect budget, pause/cancel, approval gates, execution policy stages, and company boundaries. + **Step 8 — Update status and communicate.** Always include the run ID header. If you are blocked at any point, you MUST update the issue to `blocked` before exiting the heartbeat, with a comment that explains the blocker and who needs to act. @@ -293,6 +301,9 @@ If you are asked to create or manage routines you MUST read: - **Honor "send it back to me" requests from board users.** If a board/user asks for review handoff (e.g. "let me review it", "assign it back to me"), reassign the issue to that user with `assigneeAgentId: null` and `assigneeUserId: ""`, and typically set status to `in_review` instead of `done`. Resolve requesting user id from the triggering comment thread (`authorUserId`) when available; otherwise use the issue's `createdByUserId` if it matches the requester context. - **Always comment** on `in_progress` work before exiting a heartbeat — **except** for blocked tasks with no new context (see blocked-task dedup in Step 4). +- **Start actionable work before planning-only closure.** Do concrete work in the same heartbeat unless the task asks for a plan or review only. +- **Leave a next action.** Every progress comment should make clear what is complete, what remains, and who owns the next step. +- **Prefer child issues over polling.** Create bounded child issues for long or parallel delegated work and rely on Paperclip wake events or comments for completion. - **Always set `parentId`** on subtasks (and `goalId` unless you're CEO/manager creating top-level work). - **Preserve workspace continuity for follow-ups.** Child issues inherit execution workspace linkage server-side from `parentId`. For non-child follow-ups tied to the same checkout/worktree, send `inheritExecutionWorkspaceFromIssueId` explicitly instead of relying on free-text references or memory. - **Never cancel cross-team tasks.** Reassign to your manager with a comment. diff --git a/ui/src/api/activity.ts b/ui/src/api/activity.ts index 46f887ae28..da14ff808d 100644 --- a/ui/src/api/activity.ts +++ b/ui/src/api/activity.ts @@ -1,6 +1,8 @@ -import type { ActivityEvent } from "@paperclipai/shared"; +import type { ActivityEvent, RunLivenessState } from "@paperclipai/shared"; import { api } from "./client"; +export type { RunLivenessState } from "@paperclipai/shared"; + export interface RunForIssue { runId: string; status: string; @@ -13,6 +15,11 @@ export interface RunForIssue { usageJson: Record | null; resultJson: Record | null; logBytes?: number | null; + livenessState?: RunLivenessState | null; + livenessReason?: string | null; + continuationAttempt?: number; + lastUsefulActionAt?: string | null; + nextAction?: string | null; } export interface IssueForRun { diff --git a/ui/src/api/heartbeats.ts b/ui/src/api/heartbeats.ts index 045c2b1f2f..72c8e0a700 100644 --- a/ui/src/api/heartbeats.ts +++ b/ui/src/api/heartbeats.ts @@ -1,6 +1,14 @@ import type { HeartbeatRun, HeartbeatRunEvent, InstanceSchedulerHeartbeatAgent, WorkspaceOperation } from "@paperclipai/shared"; import { api } from "./client"; +export interface RunLivenessFields { + livenessState: HeartbeatRun["livenessState"]; + livenessReason: string | null; + continuationAttempt: number; + lastUsefulActionAt: string | Date | null; + nextAction: string | null; +} + export interface ActiveRunForIssue { id: string; status: string; @@ -13,6 +21,11 @@ export interface ActiveRunForIssue { agentName: string; adapterType: string; issueId?: string | null; + livenessState?: RunLivenessFields["livenessState"]; + livenessReason?: string | null; + continuationAttempt?: number; + lastUsefulActionAt?: string | Date | null; + nextAction?: string | null; } export interface LiveRunForIssue { @@ -27,6 +40,11 @@ export interface LiveRunForIssue { agentName: string; adapterType: string; issueId?: string | null; + livenessState?: RunLivenessFields["livenessState"]; + livenessReason?: string | null; + continuationAttempt?: number; + lastUsefulActionAt?: string | null; + nextAction?: string | null; } export const heartbeatsApi = { diff --git a/ui/src/api/issues.ts b/ui/src/api/issues.ts index e0d8ec05d0..7fbaab0e35 100644 --- a/ui/src/api/issues.ts +++ b/ui/src/api/issues.ts @@ -130,7 +130,10 @@ export const issuesApi = { ), cancelComment: (id: string, commentId: string) => api.delete(`/issues/${id}/comments/${commentId}`), - listDocuments: (id: string) => api.get(`/issues/${id}/documents`), + listDocuments: (id: string, options?: { includeSystem?: boolean }) => + api.get( + `/issues/${id}/documents${options?.includeSystem ? "?includeSystem=true" : ""}`, + ), getDocument: (id: string, key: string) => api.get(`/issues/${id}/documents/${encodeURIComponent(key)}`), upsertDocument: (id: string, key: string, data: UpsertIssueDocument) => api.put(`/issues/${id}/documents/${encodeURIComponent(key)}`, data), diff --git a/ui/src/components/IssueContinuationHandoff.test.tsx b/ui/src/components/IssueContinuationHandoff.test.tsx new file mode 100644 index 0000000000..80e7a3d516 --- /dev/null +++ b/ui/src/components/IssueContinuationHandoff.test.tsx @@ -0,0 +1,107 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import type { ComponentProps } from "react"; +import { createRoot } from "react-dom/client"; +import type { IssueDocument } from "@paperclipai/shared"; +import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { IssueContinuationHandoff } from "./IssueContinuationHandoff"; + +vi.mock("./MarkdownBody", () => ({ + MarkdownBody: ({ children, className }: { children: string; className?: string }) => ( +
{children}
+ ), +})); + +vi.mock("@/components/ui/button", () => ({ + Button: ({ children, onClick, type = "button", ...props }: ComponentProps<"button">) => ( + + ), +})); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +function createHandoffDocument(): IssueDocument { + return { + id: "document-handoff", + companyId: "company-1", + issueId: "issue-1", + key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + title: "Continuation Summary", + format: "markdown", + body: "# Handoff\n\nResume from the activity tab.", + latestRevisionId: "revision-1", + latestRevisionNumber: 1, + createdByAgentId: "agent-1", + createdByUserId: null, + updatedByAgentId: "agent-1", + updatedByUserId: null, + createdAt: new Date("2026-04-19T12:00:00.000Z"), + updatedAt: new Date("2026-04-19T12:05:00.000Z"), + }; +} + +describe("IssueContinuationHandoff", () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText: vi.fn(async () => undefined) }, + }); + }); + + afterEach(() => { + container.remove(); + }); + + it("renders compact metadata by default with copy access", async () => { + const root = createRoot(container); + const handoff = createHandoffDocument(); + + await act(async () => { + root.render(); + }); + + expect(container.textContent).toContain("Continuation Summary"); + expect(container.textContent).toContain("handoff"); + expect(container.textContent).not.toContain("Resume from the activity tab."); + + const copyButton = Array.from(container.querySelectorAll("button")) + .find((button) => button.textContent?.includes("Copy")); + expect(copyButton).toBeTruthy(); + + await act(async () => { + copyButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(handoff.body); + expect(container.textContent).toContain("Copied"); + + await act(async () => { + root.unmount(); + }); + }); + + it("expands and anchors the handoff body when focused from a document deep link", async () => { + const root = createRoot(container); + const scrollIntoView = vi.fn(); + Element.prototype.scrollIntoView = scrollIntoView; + + await act(async () => { + root.render(); + }); + + expect(container.querySelector(`#document-${ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY}`)).toBeTruthy(); + expect(container.textContent).toContain("Resume from the activity tab."); + expect(scrollIntoView).toHaveBeenCalled(); + + await act(async () => { + root.unmount(); + }); + }); +}); diff --git a/ui/src/components/IssueContinuationHandoff.tsx b/ui/src/components/IssueContinuationHandoff.tsx new file mode 100644 index 0000000000..a1830a78c1 --- /dev/null +++ b/ui/src/components/IssueContinuationHandoff.tsx @@ -0,0 +1,101 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import type { IssueDocument } from "@paperclipai/shared"; +import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared"; +import { Button } from "@/components/ui/button"; +import { cn, relativeTime } from "../lib/utils"; +import { MarkdownBody } from "./MarkdownBody"; +import { Check, ChevronDown, ChevronRight, Copy, History } from "lucide-react"; + +type IssueContinuationHandoffProps = { + document: IssueDocument | null | undefined; + focusSignal?: number; +}; + +export function IssueContinuationHandoff({ + document, + focusSignal = 0, +}: IssueContinuationHandoffProps) { + const [expanded, setExpanded] = useState(false); + const [copied, setCopied] = useState(false); + const [highlighted, setHighlighted] = useState(false); + const rootRef = useRef(null); + const copiedTimerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (copiedTimerRef.current) { + clearTimeout(copiedTimerRef.current); + } + }; + }, []); + + useEffect(() => { + if (!document || focusSignal <= 0) return; + setExpanded(true); + setHighlighted(true); + rootRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + const timer = setTimeout(() => setHighlighted(false), 3000); + return () => clearTimeout(timer); + }, [document, focusSignal]); + + const copyBody = useCallback(async () => { + if (!document) return; + await navigator.clipboard?.writeText(document.body); + setCopied(true); + if (copiedTimerRef.current) { + clearTimeout(copiedTimerRef.current); + } + copiedTimerRef.current = setTimeout(() => setCopied(false), 1500); + }, [document]); + + if (!document) return null; + + const title = document.title?.trim() || "Continuation handoff"; + + return ( +
+
+ + +
+
+ {title} + + handoff + +
+
+ Updated {relativeTime(document.updatedAt)} + {document.latestRevisionNumber > 0 ? ` - revision ${document.latestRevisionNumber}` : ""} +
+
+ +
+ {expanded ? ( +
+ + {document.body} + +
+ ) : null} +
+ ); +} diff --git a/ui/src/components/IssueDocumentsSection.test.tsx b/ui/src/components/IssueDocumentsSection.test.tsx index 9f04df9f43..5b6c4bb107 100644 --- a/ui/src/components/IssueDocumentsSection.test.tsx +++ b/ui/src/components/IssueDocumentsSection.test.tsx @@ -5,6 +5,7 @@ import type { ComponentProps } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { DocumentRevision, Issue, IssueDocument } from "@paperclipai/shared"; +import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { IssueDocumentsSection } from "./IssueDocumentsSection"; import { queryKeys } from "../lib/queryKeys"; @@ -260,6 +261,50 @@ describe("IssueDocumentsSection", () => { container.remove(); }); + it("keeps system handoff documents out of the normal document surface", async () => { + const issue = createIssue(); + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }); + + mockIssuesApi.listDocuments.mockResolvedValue([ + createIssueDocument({ key: "plan", body: "# Plan" }), + createIssueDocument({ + id: "document-handoff", + key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, + title: "Continuation Summary", + body: "# Handoff", + }), + ]); + + await act(async () => { + root.render( + + + , + ); + }); + await flush(); + await flush(); + + expect(container.textContent).toContain("# Plan"); + expect(container.textContent).not.toContain("# Handoff"); + expect(container.querySelector(`#document-${ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY}`)).toBeNull(); + + await act(async () => { + root.unmount(); + }); + queryClient.clear(); + }); + it("shows the restored document body immediately after a revision restore", async () => { const blankLatestDocument = createIssueDocument({ body: "", diff --git a/ui/src/components/IssueDocumentsSection.tsx b/ui/src/components/IssueDocumentsSection.tsx index 0acbca9a6a..0bb6600263 100644 --- a/ui/src/components/IssueDocumentsSection.tsx +++ b/ui/src/components/IssueDocumentsSection.tsx @@ -8,6 +8,7 @@ import type { Issue, IssueDocument, } from "@paperclipai/shared"; +import { isSystemIssueDocumentKey } from "@paperclipai/shared"; import { useLocation } from "@/lib/router"; import { ApiError } from "../api/client"; import { issuesApi } from "../api/issues"; @@ -204,6 +205,7 @@ export function IssueDocumentsSection({ }, [issue.id, queryClient]); const syncDocumentCaches = useCallback((document: IssueDocument) => { + if (isSystemIssueDocumentKey(document.key)) return; queryClient.setQueryData( queryKeys.issues.documents(issue.id), (current) => { @@ -273,7 +275,7 @@ export function IssueDocumentsSection({ }); const sortedDocuments = useMemo(() => { - return [...(documents ?? [])].sort((a, b) => { + return (documents ?? []).filter((doc) => !isSystemIssueDocumentKey(doc.key)).sort((a, b) => { if (a.key === "plan" && b.key !== "plan") return -1; if (a.key !== "plan" && b.key === "plan") return 1; return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); diff --git a/ui/src/components/IssueRunLedger.test.tsx b/ui/src/components/IssueRunLedger.test.tsx new file mode 100644 index 0000000000..e268d598a3 --- /dev/null +++ b/ui/src/components/IssueRunLedger.test.tsx @@ -0,0 +1,271 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import type { ComponentProps, ReactNode } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import type { Issue, RunLivenessState } from "@paperclipai/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { RunForIssue } from "../api/activity"; +import { IssueRunLedgerContent } from "./IssueRunLedger"; + +vi.mock("@/lib/router", () => ({ + Link: ({ children, to, ...props }: { children: ReactNode; to: string } & ComponentProps<"a">) => ( + {children} + ), +})); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-04-18T20:00:00.000Z")); + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); + vi.useRealTimers(); +}); + +function render(ui: ReactNode) { + act(() => { + root.render(ui); + }); +} + +function createRun(overrides: Partial = {}): RunForIssue { + return { + runId: "run-00000000", + status: "succeeded", + agentId: "agent-1", + adapterType: "codex_local", + startedAt: "2026-04-18T19:58:00.000Z", + finishedAt: "2026-04-18T19:59:00.000Z", + createdAt: "2026-04-18T19:58:00.000Z", + invocationSource: "assignment", + usageJson: null, + resultJson: null, + livenessState: "advanced", + livenessReason: "Run produced concrete action evidence: 2 activity event(s)", + continuationAttempt: 0, + lastUsefulActionAt: "2026-04-18T19:59:00.000Z", + nextAction: null, + ...overrides, + }; +} + +function createIssue(overrides: Partial = {}): Issue { + return { + id: "issue-1", + companyId: "company-1", + projectId: null, + projectWorkspaceId: null, + goalId: null, + parentId: null, + title: "Child issue", + description: null, + status: "todo", + priority: "medium", + assigneeAgentId: null, + assigneeUserId: null, + checkoutRunId: null, + executionRunId: null, + executionAgentNameKey: null, + executionLockedAt: null, + createdByAgentId: null, + createdByUserId: null, + issueNumber: null, + identifier: "PAP-1", + requestDepth: 0, + billingCode: null, + assigneeAdapterOverrides: null, + executionWorkspaceId: null, + executionWorkspacePreference: null, + executionWorkspaceSettings: null, + startedAt: null, + completedAt: null, + cancelledAt: null, + hiddenAt: null, + createdAt: new Date("2026-04-18T19:00:00.000Z"), + updatedAt: new Date("2026-04-18T19:00:00.000Z"), + ...overrides, + }; +} + +function renderLedger(props: Partial> = {}) { + render( + , + ); +} + +describe("IssueRunLedger", () => { + it("renders every liveness state with exhausted continuation context", () => { + const states: RunLivenessState[] = [ + "advanced", + "plan_only", + "empty_response", + "blocked", + "failed", + "completed", + "needs_followup", + ]; + + renderLedger({ + runs: states.map((state, index) => + createRun({ + runId: `run-${index}0000000`, + createdAt: `2026-04-18T19:5${index}:00.000Z`, + livenessState: state, + livenessReason: state === "needs_followup" + ? "Run produced useful output but no concrete action evidence; continuation attempts exhausted" + : `state ${state}`, + continuationAttempt: state === "needs_followup" ? 3 : 0, + }), + ), + }); + + expect(container.textContent).toContain("Advanced"); + expect(container.textContent).toContain("Plan only"); + expect(container.textContent).toContain("Empty response"); + expect(container.textContent).toContain("Blocked"); + expect(container.textContent).toContain("Failed"); + expect(container.textContent).toContain("Completed"); + expect(container.textContent).toContain("Needs follow-up"); + expect(container.textContent).toContain("Exhausted"); + expect(container.textContent).toContain("Continuation attempt 3"); + }); + + it("renders historical runs without liveness metadata as unavailable", () => { + renderLedger({ + runs: [ + createRun({ + livenessState: null, + livenessReason: null, + continuationAttempt: undefined, + lastUsefulActionAt: null, + nextAction: null, + resultJson: null, + }), + ], + }); + + expect(container.textContent).toContain("No liveness data"); + expect(container.textContent).toContain("Stop Unavailable"); + expect(container.textContent).toContain("Last useful action Unavailable"); + }); + + it("shows live runs as pending final checks without missing-data language", () => { + renderLedger({ + runs: [ + createRun({ + status: "running", + finishedAt: null, + livenessState: null, + livenessReason: null, + continuationAttempt: 0, + lastUsefulActionAt: null, + nextAction: null, + resultJson: null, + }), + ], + }); + + expect(container.textContent).toContain("Running now by CodexCoder"); + expect(container.textContent).toContain("Checks after finish"); + expect(container.textContent).toContain("Last useful action No action recorded yet"); + expect(container.textContent).toContain("Stop Still running"); + expect(container.textContent).not.toContain("Liveness pending"); + expect(container.textContent).not.toContain("initial attempt"); + }); + + it("shows timeout, cancel, and budget stop reasons without raw logs", () => { + renderLedger({ + runs: [ + createRun({ + runId: "run-timeout", + resultJson: { stopReason: "timeout", timeoutFired: true, effectiveTimeoutSec: 30 }, + }), + createRun({ + runId: "run-cancel", + resultJson: { stopReason: "cancelled" }, + createdAt: "2026-04-18T19:57:00.000Z", + }), + createRun({ + runId: "run-budget", + resultJson: { stopReason: "budget_paused" }, + createdAt: "2026-04-18T19:56:00.000Z", + }), + ], + }); + + expect(container.textContent).toContain("timeout (30s timeout)"); + expect(container.textContent).toContain("cancelled"); + expect(container.textContent).toContain("budget paused"); + }); + + it("surfaces active and completed child issue summaries", () => { + renderLedger({ + childIssues: [ + createIssue({ id: "child-1", identifier: "PAP-2", title: "Implement worker handoff", status: "in_progress" }), + createIssue({ id: "child-2", identifier: "PAP-3", title: "Verify final report", status: "done" }), + createIssue({ id: "child-3", identifier: "PAP-4", title: "Cancelled experiment", status: "cancelled" }), + ], + }); + + expect(container.textContent).toContain("Child work"); + expect(container.textContent).toContain("1 active, 1 done, 1 cancelled"); + expect(container.textContent).toContain("PAP-2"); + expect(container.textContent).toContain("Implement worker handoff"); + + renderLedger({ + childIssues: [ + createIssue({ id: "child-2", identifier: "PAP-3", title: "Verify final report", status: "done" }), + createIssue({ id: "child-3", identifier: "PAP-4", title: "Cancelled experiment", status: "cancelled" }), + ], + }); + + expect(container.textContent).toContain("all 2 terminal (1 done, 1 cancelled)"); + }); + + it("uses wrapping-friendly markup for long next action text", () => { + renderLedger({ + runs: [ + createRun({ + nextAction: "Continue investigating this intentionally-long-next-action-token-that-needs-to-wrap-cleanly-on-mobile-and-desktop-without-overlapping-controls.", + }), + ], + }); + + const nextAction = [...container.querySelectorAll("span")] + .find((node) => node.textContent?.includes("intentionally-long-next-action-token")); + expect(nextAction?.className).toContain("break-words"); + expect(container.textContent).toContain("Next action:"); + }); + + it("shows when older runs are clipped from the ledger", () => { + renderLedger({ + runs: Array.from({ length: 10 }, (_, index) => + createRun({ + runId: `run-${index.toString().padStart(8, "0")}`, + createdAt: `2026-04-18T19:${String(index).padStart(2, "0")}:00.000Z`, + }), + ), + }); + + expect(container.textContent).toContain("2 older runs not shown"); + }); +}); diff --git a/ui/src/components/IssueRunLedger.tsx b/ui/src/components/IssueRunLedger.tsx new file mode 100644 index 0000000000..c441bfb513 --- /dev/null +++ b/ui/src/components/IssueRunLedger.tsx @@ -0,0 +1,440 @@ +import { useMemo } from "react"; +import type { Issue, Agent } from "@paperclipai/shared"; +import { useQuery } from "@tanstack/react-query"; +import { Link } from "@/lib/router"; +import { activityApi, type RunForIssue, type RunLivenessState } from "../api/activity"; +import { heartbeatsApi, type ActiveRunForIssue, type LiveRunForIssue } from "../api/heartbeats"; +import { cn, relativeTime } from "../lib/utils"; +import { queryKeys } from "../lib/queryKeys"; +import { keepPreviousDataForSameQueryTail } from "../lib/query-placeholder-data"; + +type IssueRunLedgerProps = { + issueId: string; + issueStatus: Issue["status"]; + childIssues: Issue[]; + agentMap: ReadonlyMap; + hasLiveRuns: boolean; +}; + +type IssueRunLedgerContentProps = { + runs: RunForIssue[]; + liveRuns?: LiveRunForIssue[]; + activeRun?: ActiveRunForIssue | null; + issueStatus: Issue["status"]; + childIssues: Issue[]; + agentMap: ReadonlyMap>; +}; + +type LedgerRun = RunForIssue & { + isLive?: boolean; + agentName?: string; +}; + +type LivenessCopy = { + label: string; + tone: string; + description: string; +}; + +const LIVENESS_COPY: Record = { + completed: { + label: "Completed", + tone: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", + description: "Issue reached a terminal state.", + }, + advanced: { + label: "Advanced", + tone: "border-cyan-500/30 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300", + description: "Run produced concrete evidence of progress.", + }, + plan_only: { + label: "Plan only", + tone: "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300", + description: "Run described future work without concrete action evidence.", + }, + empty_response: { + label: "Empty response", + tone: "border-orange-500/30 bg-orange-500/10 text-orange-700 dark:text-orange-300", + description: "Run finished without useful output.", + }, + blocked: { + label: "Blocked", + tone: "border-yellow-500/30 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300", + description: "Run or issue declared a blocker.", + }, + failed: { + label: "Failed", + tone: "border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300", + description: "Run ended unsuccessfully.", + }, + 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.", + }, +}; + +const PENDING_LIVENESS_COPY: LivenessCopy = { + label: "Checks after finish", + tone: "border-border bg-background text-muted-foreground", + description: "Liveness is evaluated after the run finishes.", +}; + +const MISSING_LIVENESS_COPY: LivenessCopy = { + label: "No liveness data", + tone: "border-border bg-background text-muted-foreground", + description: "This run has no persisted liveness classification.", +}; + +const TERMINAL_CHILD_STATUSES = new Set(["done", "cancelled"]); +const ACTIVE_RUN_STATUSES = new Set(["queued", "running"]); + +function asRecord(value: unknown): Record | 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; +} + +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) { + if (!start) return null; + const startMs = new Date(start).getTime(); + const endMs = end ? new Date(end).getTime() : Date.now(); + if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return null; + const totalSeconds = Math.max(0, Math.round((endMs - startMs) / 1000)); + 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`; + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`; +} + +function toIsoString(value: string | Date | null | undefined) { + if (!value) return null; + return value instanceof Date ? value.toISOString() : value; +} + +function liveRunToLedgerRun(run: LiveRunForIssue | ActiveRunForIssue): LedgerRun { + return { + runId: run.id, + status: run.status, + agentId: run.agentId, + agentName: run.agentName, + adapterType: run.adapterType, + startedAt: toIsoString(run.startedAt), + finishedAt: toIsoString(run.finishedAt), + createdAt: toIsoString(run.createdAt) ?? new Date().toISOString(), + invocationSource: run.invocationSource, + usageJson: null, + resultJson: null, + isLive: run.status === "queued" || run.status === "running", + }; +} + +function mergeRuns( + runs: RunForIssue[], + liveRuns: LiveRunForIssue[] | undefined, + activeRun: ActiveRunForIssue | null | undefined, +) { + const byId = new Map(); + for (const run of runs) byId.set(run.runId, run); + for (const run of liveRuns ?? []) { + const existing = byId.get(run.id); + byId.set(run.id, existing ? { ...existing, isLive: true, agentName: run.agentName } : liveRunToLedgerRun(run)); + } + if (activeRun && !byId.has(activeRun.id)) { + byId.set(activeRun.id, liveRunToLedgerRun(activeRun)); + } + + return [...byId.values()].sort((a, b) => { + const aTime = new Date(a.startedAt ?? a.createdAt).getTime(); + const bTime = new Date(b.startedAt ?? b.createdAt).getTime(); + if (aTime !== bTime) return bTime - aTime; + return b.runId.localeCompare(a.runId); + }); +} + +function statusLabel(status: string) { + return status.replace(/_/g, " "); +} + +function isActiveRun(run: Pick) { + return run.isLive || ACTIVE_RUN_STATUSES.has(run.status); +} + +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}`; + return `${statusLabel(run.status)} by ${agentName}`; +} + +function livenessCopyForRun(run: LedgerRun) { + if (run.livenessState) return LIVENESS_COPY[run.livenessState]; + return isActiveRun(run) ? PENDING_LIVENESS_COPY : MISSING_LIVENESS_COPY; +} + +function stopReasonLabel(run: RunForIssue) { + const result = asRecord(run.resultJson); + const stopReason = readString(result?.stopReason); + const timeoutFired = result?.timeoutFired === true; + const effectiveTimeoutSec = readNumber(result?.effectiveTimeoutSec); + const timeoutText = + effectiveTimeoutSec && effectiveTimeoutSec > 0 ? `${effectiveTimeoutSec}s timeout` : null; + + if (timeoutFired || stopReason === "timeout") { + return timeoutText ? `timeout (${timeoutText})` : "timeout"; + } + if (stopReason === "budget_paused") return "budget paused"; + if (stopReason === "cancelled") return "cancelled"; + if (stopReason === "paused") return "paused"; + if (stopReason === "process_lost") return "process lost"; + if (stopReason === "adapter_failed") return "adapter failed"; + if (stopReason === "completed") return timeoutText ? `completed (${timeoutText})` : "completed"; + return timeoutText; +} + +function stopStatusLabel(run: LedgerRun, stopReason: string | null) { + if (stopReason) return stopReason; + if (run.status === "queued") return "Waiting to start"; + if (run.status === "running") return "Still running"; + if (!run.livenessState) return "Unavailable"; + return "No stop reason"; +} + +function lastUsefulActionLabel(run: LedgerRun) { + if (run.lastUsefulActionAt) return relativeTime(run.lastUsefulActionAt); + if (isActiveRun(run)) return "No action recorded yet"; + if (run.livenessState === "plan_only" || run.livenessState === "needs_followup") { + return "No concrete action"; + } + if (run.livenessState === "empty_response") return "No useful output"; + if (!run.livenessState) return "Unavailable"; + return "None recorded"; +} + +function continuationLabel(run: LedgerRun) { + if (!run.continuationAttempt || run.continuationAttempt <= 0) return null; + return `Continuation attempt ${run.continuationAttempt}`; +} + +function hasExhaustedContinuation(run: RunForIssue) { + return /continuation attempts exhausted/i.test(run.livenessReason ?? ""); +} + +function childIssueSummary(childIssues: Issue[]) { + 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; + 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); +} + +export function IssueRunLedger({ + issueId, + issueStatus, + childIssues, + agentMap, + hasLiveRuns, +}: IssueRunLedgerProps) { + const { data: runs } = useQuery({ + queryKey: queryKeys.issues.runs(issueId), + queryFn: () => activityApi.runsForIssue(issueId), + refetchInterval: hasLiveRuns ? 5000 : false, + placeholderData: keepPreviousDataForSameQueryTail(issueId), + }); + const { data: liveRuns } = useQuery({ + queryKey: queryKeys.issues.liveRuns(issueId), + queryFn: () => heartbeatsApi.liveRunsForIssue(issueId), + enabled: hasLiveRuns, + refetchInterval: 3000, + 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), + }); + + return ( + + ); +} + +export function IssueRunLedgerContent({ + runs, + liveRuns, + activeRun, + issueStatus, + childIssues, + agentMap, +}: IssueRunLedgerContentProps) { + const ledgerRuns = useMemo(() => mergeRuns(runs, liveRuns, activeRun), [activeRun, liveRuns, runs]); + const latestRun = ledgerRuns[0] ?? null; + const children = childIssueSummary(childIssues); + + return ( +
+
+
+

Run ledger

+

+ {latestRun + ? runSummary(latestRun, agentMap) + : issueStatus === "in_progress" + ? "Waiting for the first run record." + : "No runs linked yet."} +

+
+ {latestRun ? ( + + Latest run + + ) : null} +
+ + {children.total > 0 ? ( +
+
+ Child work + + {children.active.length > 0 + ? `${children.active.length} active, ${children.done} done, ${children.cancelled} cancelled` + : `all ${children.total} terminal (${children.done} done, ${children.cancelled} cancelled)`} + +
+ {children.active.length > 0 ? ( +
+ {children.active.slice(0, 4).map((child) => ( + + {child.identifier ?? child.id.slice(0, 8)} + {child.title} + {statusLabel(child.status)} + + ))} + {children.active.length > 4 ? ( + + +{children.active.length - 4} more + + ) : null} +
+ ) : null} +
+ ) : null} + + {ledgerRuns.length === 0 ? ( +
+ Historical runs without liveness metadata will appear here once linked to this issue. +
+ ) : ( +
+ {ledgerRuns.slice(0, 8).map((run) => { + const liveness = livenessCopyForRun(run); + const stopReason = stopReasonLabel(run); + const duration = formatDuration(run.startedAt, run.finishedAt); + const exhausted = hasExhaustedContinuation(run); + const continuation = continuationLabel(run); + return ( +
+
+ + {run.runId.slice(0, 8)} + + + {statusLabel(run.status)} + + {run.isLive ? ( + + + live + + ) : null} + + {liveness.label} + + {exhausted ? ( + + Exhausted + + ) : null} + {continuation ? ( + {continuation} + ) : null} +
+ +
+
+ Elapsed{" "} + {duration ?? "unknown"} +
+
+ Last useful action{" "} + {lastUsefulActionLabel(run)} +
+
+ Stop{" "} + {stopStatusLabel(run, stopReason)} +
+
+ + {run.livenessReason ? ( +

+ {run.livenessReason} +

+ ) : null} + + {run.nextAction ? ( +
+ Next action: + {run.nextAction} +
+ ) : null} +
+ ); + })} + {ledgerRuns.length > 8 ? ( +
+ {ledgerRuns.length - 8} older runs not shown +
+ ) : null} +
+ )} +
+ ); +} diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 23c6973016..e115e3fe05 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -154,6 +154,11 @@ function makeRun(id: string, status: HeartbeatRun["status"], createdAt: string, processStartedAt: null, retryOfRunId: null, processLossRetryCount: 0, + livenessState: null, + livenessReason: null, + continuationAttempt: 0, + lastUsefulActionAt: null, + nextAction: null, stdoutExcerpt: null, stderrExcerpt: null, contextSnapshot: null, diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index 57c4abb522..1d83563cdd 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -48,6 +48,7 @@ export const queryKeys = { feedbackVotes: (issueId: string) => ["issues", "feedback-votes", issueId] as const, attachments: (issueId: string) => ["issues", "attachments", issueId] 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, activity: (issueId: string) => ["issues", "activity", issueId] as const, runs: (issueId: string) => ["issues", "runs", issueId] as const, diff --git a/ui/src/pages/Inbox.test.tsx b/ui/src/pages/Inbox.test.tsx index 77e7de9540..8f6c4c43d3 100644 --- a/ui/src/pages/Inbox.test.tsx +++ b/ui/src/pages/Inbox.test.tsx @@ -148,6 +148,11 @@ describe("FailedRunInboxRow", () => { processStartedAt: null, retryOfRunId: null, processLossRetryCount: 0, + livenessState: null, + livenessReason: null, + continuationAttempt: 0, + lastUsefulActionAt: null, + nextAction: null, stdoutExcerpt: null, stderrExcerpt: null, contextSnapshot: null, diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 4ee14f7660..dd00e43f8e 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState, type ChangeEve import { pickTextColorForPillBg } from "@/lib/color-contrast"; import { Link, useLocation, useNavigate, useNavigationType, useParams } from "@/lib/router"; import { useInfiniteQuery, useQuery, useMutation, useQueryClient, type InfiniteData, type QueryClient } from "@tanstack/react-query"; +import { ApiError } from "../api/client"; import { issuesApi } from "../api/issues"; import { approvalsApi } from "../api/approvals"; import { activityApi, type RunForIssue } from "../api/activity"; @@ -59,9 +60,11 @@ import { relativeTime, cn, formatTokens, visibleRunCostUsd } from "../lib/utils" import { ApprovalCard } from "../components/ApprovalCard"; import { InlineEditor } from "../components/InlineEditor"; import { IssueChatThread, type IssueChatComposerHandle } from "../components/IssueChatThread"; +import { IssueContinuationHandoff } from "../components/IssueContinuationHandoff"; import { IssueDocumentsSection } from "../components/IssueDocumentsSection"; import { IssuesList } from "../components/IssuesList"; import { IssueProperties } from "../components/IssueProperties"; +import { IssueRunLedger } from "../components/IssueRunLedger"; import { IssueWorkspaceCard } from "../components/IssueWorkspaceCard"; import type { MentionOption } from "../components/MarkdownEditor"; import { ImageGalleryModal } from "../components/ImageGalleryModal"; @@ -103,6 +106,7 @@ import { import { getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, + ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, type ActivityEvent, type Agent, type FeedbackVote, @@ -722,20 +726,28 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ type IssueDetailActivityTabProps = { issueId: string; + issueStatus: Issue["status"]; + childIssues: Issue[]; agentMap: Map; + hasLiveRuns: boolean; currentUserId: string | null; userProfileMap: Map; pendingApprovalAction: { approvalId: string; action: "approve" | "reject" } | null; onApprovalAction: (approvalId: string, action: "approve" | "reject") => void; + handoffFocusSignal?: number; }; function IssueDetailActivityTab({ issueId, + issueStatus, + childIssues, agentMap, + hasLiveRuns, currentUserId, userProfileMap, pendingApprovalAction, onApprovalAction, + handoffFocusSignal = 0, }: IssueDetailActivityTabProps) { const { data: activity, isLoading: activityLoading } = useQuery({ queryKey: queryKeys.issues.activity(issueId), @@ -752,6 +764,21 @@ function IssueDetailActivityTab({ queryFn: () => issuesApi.listApprovals(issueId), placeholderData: keepPreviousDataForSameQueryTail>>(issueId), }); + const { data: continuationHandoff } = useQuery({ + queryKey: queryKeys.issues.document(issueId, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY), + queryFn: async () => { + try { + return await issuesApi.getDocument(issueId, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY); + } catch (error) { + if (error instanceof ApiError && error.status === 404) return null; + throw error; + } + }, + retry: false, + placeholderData: keepPreviousDataForSameQueryTail> | null>( + issueId, + ), + }); const initialLoading = (activityLoading && activity === undefined) || (linkedRunsLoading && linkedRuns === undefined); @@ -800,6 +827,16 @@ function IssueDetailActivityTab({ return ( <> +
+ +
+ {linkedApprovals && linkedApprovals.length > 0 && (
{linkedApprovals.map((approval) => ( @@ -877,6 +914,7 @@ export function IssueDetail() { const [copied, setCopied] = useState(false); const [mobilePropsOpen, setMobilePropsOpen] = useState(false); const [detailTab, setDetailTab] = useState("chat"); + const [handoffFocusSignal, setHandoffFocusSignal] = useState(0); const [pendingApprovalAction, setPendingApprovalAction] = useState<{ approvalId: string; action: "approve" | "reject"; @@ -1960,6 +1998,15 @@ export function IssueDetail() { }; }, [keyboardShortcutsEnabled, navigate, sourceBreadcrumb.href]); + useEffect(() => { + const hash = location.hash; + if (!hash.startsWith("#document-")) return; + const documentKey = decodeURIComponent(hash.slice("#document-".length)); + if (documentKey !== ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY) return; + setDetailTab("activity"); + setHandoffFocusSignal((current) => current + 1); + }, [location.hash]); + useEffect(() => { if (pendingCommentComposerFocusKey === 0) return; if (detailTab !== "chat") return; @@ -2661,10 +2708,14 @@ export function IssueDetail() { {detailTab === "activity" ? ( { approvalDecision.mutate({ approvalId, action }); }} diff --git a/vitest.config.ts b/vitest.config.ts index 3ec05081ba..e2378ea8e9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ test: { projects: [ "packages/db", + "packages/adapter-utils", "packages/adapters/codex-local", "packages/adapters/opencode-local", "server",