From c0d0d03bce37965cb6a14d3f31d9ca90df7fe6ef Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 2 Apr 2026 09:11:49 -0500 Subject: [PATCH 1/2] Add feedback voting and thumbs capture flow Co-Authored-By: Paperclip --- .gitignore | 1 + cli/src/__tests__/company-delete.test.ts | 4 + cli/src/__tests__/company.test.ts | 8 + cli/src/__tests__/feedback.test.ts | 177 + cli/src/commands/client/company.ts | 131 + cli/src/commands/client/feedback.ts | 621 + cli/src/commands/client/issue.ts | 119 + cli/src/index.ts | 2 + docs/feedback-voting.md | 189 + .../src/migrations/0047_overjoyed_groot.sql | 70 + .../db/src/migrations/meta/0047_snapshot.json | 12539 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema/companies.ts | 6 + packages/db/src/schema/document_revisions.ts | 2 + packages/db/src/schema/feedback_exports.ts | 45 + packages/db/src/schema/feedback_votes.ts | 34 + packages/db/src/schema/index.ts | 2 + packages/db/src/schema/issue_comments.ts | 2 + packages/shared/src/index.ts | 24 + .../shared/src/types/company-portability.ts | 4 + packages/shared/src/types/company.ts | 4 + packages/shared/src/types/feedback.ts | 120 + packages/shared/src/types/index.ts | 12 + packages/shared/src/types/instance.ts | 3 + .../src/validators/company-portability.ts | 4 + packages/shared/src/validators/company.ts | 5 + packages/shared/src/validators/feedback.ts | 22 + packages/shared/src/validators/index.ts | 8 + packages/shared/src/validators/instance.ts | 5 + .../src/__tests__/agent-skills-routes.test.ts | 2 +- .../companies-route-path-guard.test.ts | 6 + .../__tests__/company-branding-route.test.ts | 11 +- .../company-portability-routes.test.ts | 7 + server/src/__tests__/feedback-service.test.ts | 1103 ++ .../instance-settings-routes.test.ts | 15 +- .../issue-comment-reopen-routes.test.ts | 14 + .../issue-document-restore-routes.test.ts | 5 + .../issues-goal-context-routes.test.ts | 14 + server/src/__tests__/issues-service.test.ts | 50 +- .../server-startup-feedback-export.test.ts | 173 + .../src/__tests__/workspace-runtime.test.ts | 20 +- server/src/app.ts | 22 + server/src/config.ts | 12 + server/src/index.ts | 12 +- server/src/routes/companies.ts | 66 +- server/src/routes/issues.ts | 192 + server/src/services/companies.ts | 4 + server/src/services/company-portability.ts | 36 +- server/src/services/documents.ts | 3 + server/src/services/feedback-redaction.ts | 193 + server/src/services/feedback-share-client.ts | 54 + server/src/services/feedback.ts | 2045 +++ server/src/services/heartbeat.ts | 4 +- server/src/services/index.ts | 1 + server/src/services/instance-settings.ts | 4 + server/src/services/issues.ts | 58 +- ui/src/api/companies.ts | 9 +- ui/src/api/issues.ts | 23 + ui/src/components/CommentThread.tsx | 108 +- ui/src/components/IssueDocumentsSection.tsx | 42 +- ui/src/components/OutputFeedbackButtons.tsx | 259 + ui/src/lib/queryKeys.ts | 1 + ui/src/pages/CompanySettings.tsx | 66 + ui/src/pages/InstanceGeneralSettings.tsx | 92 +- ui/src/pages/IssueDetail.tsx | 169 +- ui/src/vite-env.d.ts | 1 + 66 files changed, 18988 insertions(+), 78 deletions(-) create mode 100644 cli/src/__tests__/feedback.test.ts create mode 100644 cli/src/commands/client/feedback.ts create mode 100644 docs/feedback-voting.md create mode 100644 packages/db/src/migrations/0047_overjoyed_groot.sql create mode 100644 packages/db/src/migrations/meta/0047_snapshot.json create mode 100644 packages/db/src/schema/feedback_exports.ts create mode 100644 packages/db/src/schema/feedback_votes.ts create mode 100644 packages/shared/src/types/feedback.ts create mode 100644 packages/shared/src/validators/feedback.ts create mode 100644 server/src/__tests__/feedback-service.test.ts create mode 100644 server/src/__tests__/server-startup-feedback-export.test.ts create mode 100644 server/src/services/feedback-redaction.ts create mode 100644 server/src/services/feedback-share-client.ts create mode 100644 server/src/services/feedback.ts create mode 100644 ui/src/components/OutputFeedbackButtons.tsx create mode 100644 ui/src/vite-env.d.ts diff --git a/.gitignore b/.gitignore index 61b00a22..f685aca6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ server/src/**/*.js.map server/src/**/*.d.ts server/src/**/*.d.ts.map tmp/ +feedback-export-* # Editor / tool temp files *.tmp diff --git a/cli/src/__tests__/company-delete.test.ts b/cli/src/__tests__/company-delete.test.ts index 73d5e440..d6fc399f 100644 --- a/cli/src/__tests__/company-delete.test.ts +++ b/cli/src/__tests__/company-delete.test.ts @@ -15,6 +15,10 @@ function makeCompany(overrides: Partial): Company { budgetMonthlyCents: 0, spentMonthlyCents: 0, requireBoardApprovalForNewAgents: false, + feedbackDataSharingEnabled: false, + feedbackDataSharingConsentAt: null, + feedbackDataSharingConsentByUserId: null, + feedbackDataSharingTermsVersion: null, brandColor: null, logoAssetId: null, logoUrl: null, diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index d74674b2..268d1266 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -163,6 +163,10 @@ describe("renderCompanyImportPreview", () => { brandColor: null, logoPath: null, requireBoardApprovalForNewAgents: false, + feedbackDataSharingEnabled: false, + feedbackDataSharingConsentAt: null, + feedbackDataSharingConsentByUserId: null, + feedbackDataSharingTermsVersion: null, }, sidebar: { agents: ["ceo"], @@ -371,6 +375,10 @@ describe("import selection catalog", () => { brandColor: null, logoPath: "images/company-logo.png", requireBoardApprovalForNewAgents: false, + feedbackDataSharingEnabled: false, + feedbackDataSharingConsentAt: null, + feedbackDataSharingConsentByUserId: null, + feedbackDataSharingTermsVersion: null, }, sidebar: { agents: ["ceo"], diff --git a/cli/src/__tests__/feedback.test.ts b/cli/src/__tests__/feedback.test.ts new file mode 100644 index 00000000..e46b307d --- /dev/null +++ b/cli/src/__tests__/feedback.test.ts @@ -0,0 +1,177 @@ +import os from "node:os"; +import path from "node:path"; +import { mkdtemp, readFile } from "node:fs/promises"; +import { Command } from "commander"; +import { describe, expect, it } from "vitest"; +import type { FeedbackTrace } from "@paperclipai/shared"; +import { readZipArchive } from "../commands/client/zip.js"; +import { + buildFeedbackTraceQuery, + registerFeedbackCommands, + renderFeedbackReport, + summarizeFeedbackTraces, + writeFeedbackExportBundle, +} from "../commands/client/feedback.js"; + +function makeTrace(overrides: Partial = {}): FeedbackTrace { + return { + id: "trace-12345678", + companyId: "company-123", + feedbackVoteId: "vote-12345678", + issueId: "issue-123", + projectId: "project-123", + issueIdentifier: "PAP-123", + issueTitle: "Fix the feedback command", + authorUserId: "user-123", + targetType: "issue_comment", + targetId: "comment-123", + vote: "down", + status: "pending", + destination: "paperclip_labs_feedback_v1", + exportId: null, + consentVersion: "feedback-data-sharing-v1", + schemaVersion: "1", + bundleVersion: "1", + payloadVersion: "1", + payloadDigest: null, + payloadSnapshot: { + vote: { + value: "down", + reason: "Needed more detail", + }, + }, + targetSummary: { + label: "Comment", + excerpt: "The first answer was too vague.", + authorAgentId: "agent-123", + authorUserId: null, + createdAt: new Date("2026-03-31T12:00:00.000Z"), + documentKey: null, + documentTitle: null, + revisionNumber: null, + }, + redactionSummary: null, + attemptCount: 0, + lastAttemptedAt: null, + exportedAt: null, + failureReason: null, + createdAt: new Date("2026-03-31T12:01:00.000Z"), + updatedAt: new Date("2026-03-31T12:02:00.000Z"), + ...overrides, + }; +} + +describe("registerFeedbackCommands", () => { + it("registers the top-level feedback commands", () => { + const program = new Command(); + + expect(() => registerFeedbackCommands(program)).not.toThrow(); + + const feedback = program.commands.find((command) => command.name() === "feedback"); + expect(feedback).toBeDefined(); + expect(feedback?.commands.map((command) => command.name())).toEqual(["report", "export"]); + expect(feedback?.commands[0]?.options.filter((option) => option.long === "--company-id")).toHaveLength(1); + }); +}); + +describe("buildFeedbackTraceQuery", () => { + it("encodes all supported filters", () => { + expect( + buildFeedbackTraceQuery({ + targetType: "issue_comment", + vote: "down", + status: "pending", + projectId: "project-123", + issueId: "issue-123", + from: "2026-03-31T00:00:00.000Z", + to: "2026-03-31T23:59:59.999Z", + sharedOnly: true, + }), + ).toBe( + "?targetType=issue_comment&vote=down&status=pending&projectId=project-123&issueId=issue-123&from=2026-03-31T00%3A00%3A00.000Z&to=2026-03-31T23%3A59%3A59.999Z&sharedOnly=true&includePayload=true", + ); + }); +}); + +describe("renderFeedbackReport", () => { + it("includes summary counts and the optional reason", () => { + const traces = [ + makeTrace(), + makeTrace({ + id: "trace-87654321", + feedbackVoteId: "vote-87654321", + vote: "up", + status: "local_only", + payloadSnapshot: { + vote: { + value: "up", + reason: null, + }, + }, + }), + ]; + + const report = renderFeedbackReport({ + apiBase: "http://127.0.0.1:3100", + companyId: "company-123", + traces, + summary: summarizeFeedbackTraces(traces), + includePayloads: false, + }); + + expect(report).toContain("Paperclip Feedback Report"); + expect(report).toContain("thumbs up"); + expect(report).toContain("thumbs down"); + expect(report).toContain("Needed more detail"); + }); +}); + +describe("writeFeedbackExportBundle", () => { + it("writes votes, traces, a manifest, and a zip archive", async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-feedback-export-")); + const outputDir = path.join(tempDir, "feedback-export"); + const traces = [ + makeTrace(), + makeTrace({ + id: "trace-abcdef12", + feedbackVoteId: "vote-abcdef12", + issueIdentifier: "PAP-124", + issueId: "issue-124", + vote: "up", + status: "local_only", + payloadSnapshot: { + vote: { + value: "up", + reason: null, + }, + }, + }), + ]; + + const exported = await writeFeedbackExportBundle({ + apiBase: "http://127.0.0.1:3100", + companyId: "company-123", + traces, + outputDir, + }); + + expect(exported.manifest.summary.total).toBe(2); + expect(exported.manifest.summary.withReason).toBe(1); + + const manifest = JSON.parse(await readFile(path.join(outputDir, "index.json"), "utf8")) as { + files: { votes: string[]; traces: string[]; zip: string }; + }; + expect(manifest.files.votes).toHaveLength(2); + expect(manifest.files.traces).toHaveLength(2); + + const archive = await readFile(exported.zipPath); + const zip = await readZipArchive(archive); + expect(Object.keys(zip.files)).toEqual( + expect.arrayContaining([ + "index.json", + `votes/${manifest.files.votes[0]}`, + `traces/${manifest.files.traces[0]}`, + ]), + ); + }); +}); diff --git a/cli/src/commands/client/company.ts b/cli/src/commands/client/company.ts index 571c1c1a..b24cf1d8 100644 --- a/cli/src/commands/client/company.ts +++ b/cli/src/commands/client/company.ts @@ -5,6 +5,7 @@ import * as p from "@clack/prompts"; import pc from "picocolors"; import type { Company, + FeedbackTrace, CompanyPortabilityFileEntry, CompanyPortabilityExportResult, CompanyPortabilityInclude, @@ -44,6 +45,20 @@ interface CompanyExportOptions extends BaseClientOptions { expandReferencedSkills?: boolean; } +interface CompanyFeedbackOptions extends BaseClientOptions { + targetType?: string; + vote?: string; + status?: string; + projectId?: string; + issueId?: string; + from?: string; + to?: string; + sharedOnly?: boolean; + includePayload?: boolean; + out?: string; + format?: string; +} + interface CompanyImportOptions extends BaseClientOptions { include?: string; target?: CompanyImportTargetMode; @@ -165,6 +180,34 @@ function parseCsvValues(input: string | undefined): string[] { return Array.from(new Set(input.split(",").map((part) => part.trim()).filter(Boolean))); } +function buildFeedbackTraceQuery(opts: CompanyFeedbackOptions): string { + const params = new URLSearchParams(); + if (opts.targetType) params.set("targetType", opts.targetType); + if (opts.vote) params.set("vote", opts.vote); + if (opts.status) params.set("status", opts.status); + if (opts.projectId) params.set("projectId", opts.projectId); + if (opts.issueId) params.set("issueId", opts.issueId); + if (opts.from) params.set("from", opts.from); + if (opts.to) params.set("to", opts.to); + if (opts.sharedOnly) params.set("sharedOnly", "true"); + if (opts.includePayload) params.set("includePayload", "true"); + const query = params.toString(); + return query ? `?${query}` : ""; +} + +function normalizeFeedbackExportFormat(value: string | undefined): "json" | "ndjson" { + if (!value || value === "ndjson") return "ndjson"; + if (value === "json") return "json"; + throw new Error(`Unsupported export format: ${value}`); +} + +function serializeFeedbackTraces(traces: FeedbackTrace[], format: string | undefined): string { + if (normalizeFeedbackExportFormat(format) === "json") { + return JSON.stringify(traces, null, 2); + } + return traces.map((trace) => JSON.stringify(trace)).join("\n"); +} + function isInteractiveTerminal(): boolean { return Boolean(process.stdin.isTTY && process.stdout.isTTY); } @@ -1103,6 +1146,94 @@ export function registerCompanyCommands(program: Command): void { }), ); + addCommonClientOptions( + company + .command("feedback:list") + .description("List feedback traces for a company") + .requiredOption("-C, --company-id ", "Company ID") + .option("--target-type ", "Filter by target type") + .option("--vote ", "Filter by vote value") + .option("--status ", "Filter by trace status") + .option("--project-id ", "Filter by project ID") + .option("--issue-id ", "Filter by issue ID") + .option("--from ", "Only include traces created at or after this timestamp") + .option("--to ", "Only include traces created at or before this timestamp") + .option("--shared-only", "Only include traces eligible for sharing/export") + .option("--include-payload", "Include stored payload snapshots in the response") + .action(async (opts: CompanyFeedbackOptions) => { + try { + const ctx = resolveCommandContext(opts, { requireCompany: true }); + const traces = (await ctx.api.get( + `/api/companies/${ctx.companyId}/feedback-traces${buildFeedbackTraceQuery(opts)}`, + )) ?? []; + if (ctx.json) { + printOutput(traces, { json: true }); + return; + } + printOutput( + traces.map((trace) => ({ + id: trace.id, + issue: trace.issueIdentifier ?? trace.issueId, + vote: trace.vote, + status: trace.status, + targetType: trace.targetType, + target: trace.targetSummary.label, + })), + { json: false }, + ); + } catch (err) { + handleCommandError(err); + } + }), + { includeCompany: false }, + ); + + addCommonClientOptions( + company + .command("feedback:export") + .description("Export feedback traces for a company") + .requiredOption("-C, --company-id ", "Company ID") + .option("--target-type ", "Filter by target type") + .option("--vote ", "Filter by vote value") + .option("--status ", "Filter by trace status") + .option("--project-id ", "Filter by project ID") + .option("--issue-id ", "Filter by issue ID") + .option("--from ", "Only include traces created at or after this timestamp") + .option("--to ", "Only include traces created at or before this timestamp") + .option("--shared-only", "Only include traces eligible for sharing/export") + .option("--include-payload", "Include stored payload snapshots in the export") + .option("--out ", "Write export to a file path instead of stdout") + .option("--format ", "Export format: json or ndjson", "ndjson") + .action(async (opts: CompanyFeedbackOptions) => { + try { + const ctx = resolveCommandContext(opts, { requireCompany: true }); + const traces = (await ctx.api.get( + `/api/companies/${ctx.companyId}/feedback-traces${buildFeedbackTraceQuery({ + ...opts, + includePayload: opts.includePayload ?? true, + })}`, + )) ?? []; + const serialized = serializeFeedbackTraces(traces, opts.format); + if (opts.out?.trim()) { + await writeFile(opts.out, serialized, "utf8"); + if (ctx.json) { + printOutput( + { out: opts.out, count: traces.length, format: normalizeFeedbackExportFormat(opts.format) }, + { json: true }, + ); + return; + } + console.log(`Wrote ${traces.length} feedback trace(s) to ${opts.out}`); + return; + } + process.stdout.write(`${serialized}${serialized.endsWith("\n") ? "" : "\n"}`); + } catch (err) { + handleCommandError(err); + } + }), + { includeCompany: false }, + ); + addCommonClientOptions( company .command("export") diff --git a/cli/src/commands/client/feedback.ts b/cli/src/commands/client/feedback.ts new file mode 100644 index 00000000..d2f0f71b --- /dev/null +++ b/cli/src/commands/client/feedback.ts @@ -0,0 +1,621 @@ +import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; +import pc from "picocolors"; +import { Command } from "commander"; +import type { Company, FeedbackTrace, FeedbackTraceBundle } from "@paperclipai/shared"; +import { + addCommonClientOptions, + handleCommandError, + printOutput, + resolveCommandContext, + type BaseClientOptions, + type ResolvedClientContext, +} from "./common.js"; + +interface FeedbackFilterOptions extends BaseClientOptions { + targetType?: string; + vote?: string; + status?: string; + projectId?: string; + issueId?: string; + from?: string; + to?: string; + sharedOnly?: boolean; +} + +interface FeedbackReportOptions extends FeedbackFilterOptions { + payloads?: boolean; +} + +interface FeedbackExportOptions extends FeedbackFilterOptions { + out?: string; +} + +interface FeedbackSummary { + total: number; + thumbsUp: number; + thumbsDown: number; + withReason: number; + statuses: Record; +} + +interface FeedbackExportManifest { + exportedAt: string; + serverUrl: string; + companyId: string; + summary: FeedbackSummary & { + uniqueIssues: number; + issues: string[]; + }; + files: { + votes: string[]; + traces: string[]; + fullTraces: string[]; + zip: string; + }; +} + +interface FeedbackExportResult { + outputDir: string; + zipPath: string; + manifest: FeedbackExportManifest; +} + +export function registerFeedbackCommands(program: Command): void { + const feedback = program.command("feedback").description("Inspect and export local feedback traces"); + + addCommonClientOptions( + feedback + .command("report") + .description("Render a terminal report for company feedback traces") + .option("-C, --company-id ", "Company ID (overrides context default)") + .option("--target-type ", "Filter by target type") + .option("--vote ", "Filter by vote value") + .option("--status ", "Filter by trace status") + .option("--project-id ", "Filter by project ID") + .option("--issue-id ", "Filter by issue ID") + .option("--from ", "Only include traces created at or after this timestamp") + .option("--to ", "Only include traces created at or before this timestamp") + .option("--shared-only", "Only include traces eligible for sharing/export") + .option("--payloads", "Include raw payload dumps in the terminal report", false) + .action(async (opts: FeedbackReportOptions) => { + try { + const ctx = resolveCommandContext(opts); + const companyId = await resolveFeedbackCompanyId(ctx, opts.companyId); + const traces = await fetchCompanyFeedbackTraces(ctx, companyId, opts); + const summary = summarizeFeedbackTraces(traces); + if (ctx.json) { + printOutput( + { + apiBase: ctx.api.apiBase, + companyId, + summary, + traces, + }, + { json: true }, + ); + return; + } + console.log(renderFeedbackReport({ + apiBase: ctx.api.apiBase, + companyId, + traces, + summary, + includePayloads: Boolean(opts.payloads), + })); + } catch (err) { + handleCommandError(err); + } + }), + { includeCompany: false }, + ); + + addCommonClientOptions( + feedback + .command("export") + .description("Export feedback votes and raw trace bundles into a folder plus zip archive") + .option("-C, --company-id ", "Company ID (overrides context default)") + .option("--target-type ", "Filter by target type") + .option("--vote ", "Filter by vote value") + .option("--status ", "Filter by trace status") + .option("--project-id ", "Filter by project ID") + .option("--issue-id ", "Filter by issue ID") + .option("--from ", "Only include traces created at or after this timestamp") + .option("--to ", "Only include traces created at or before this timestamp") + .option("--shared-only", "Only include traces eligible for sharing/export") + .option("--out ", "Output directory (default: ./feedback-export-)") + .action(async (opts: FeedbackExportOptions) => { + try { + const ctx = resolveCommandContext(opts); + const companyId = await resolveFeedbackCompanyId(ctx, opts.companyId); + const traces = await fetchCompanyFeedbackTraces(ctx, companyId, opts); + const outputDir = path.resolve(opts.out?.trim() || defaultFeedbackExportDirName()); + const exported = await writeFeedbackExportBundle({ + apiBase: ctx.api.apiBase, + companyId, + traces, + outputDir, + traceBundleFetcher: (trace) => fetchFeedbackTraceBundle(ctx, trace.id), + }); + if (ctx.json) { + printOutput( + { + companyId, + outputDir: exported.outputDir, + zipPath: exported.zipPath, + summary: exported.manifest.summary, + }, + { json: true }, + ); + return; + } + console.log(renderFeedbackExportSummary(exported)); + } catch (err) { + handleCommandError(err); + } + }), + { includeCompany: false }, + ); +} + +export async function resolveFeedbackCompanyId( + ctx: ResolvedClientContext, + explicitCompanyId?: string, +): Promise { + const direct = explicitCompanyId?.trim() || ctx.companyId?.trim(); + if (direct) return direct; + const companies = (await ctx.api.get("/api/companies")) ?? []; + const companyId = companies[0]?.id?.trim(); + if (!companyId) { + throw new Error( + "Company ID is required. Pass --company-id, set PAPERCLIP_COMPANY_ID, or configure a CLI context default.", + ); + } + return companyId; +} + +export function buildFeedbackTraceQuery(opts: FeedbackFilterOptions, includePayload = true): string { + const params = new URLSearchParams(); + if (opts.targetType) params.set("targetType", opts.targetType); + if (opts.vote) params.set("vote", opts.vote); + if (opts.status) params.set("status", opts.status); + if (opts.projectId) params.set("projectId", opts.projectId); + if (opts.issueId) params.set("issueId", opts.issueId); + if (opts.from) params.set("from", opts.from); + if (opts.to) params.set("to", opts.to); + if (opts.sharedOnly) params.set("sharedOnly", "true"); + if (includePayload) params.set("includePayload", "true"); + const query = params.toString(); + return query ? `?${query}` : ""; +} + +export async function fetchCompanyFeedbackTraces( + ctx: ResolvedClientContext, + companyId: string, + opts: FeedbackFilterOptions, +): Promise { + return ( + (await ctx.api.get( + `/api/companies/${companyId}/feedback-traces${buildFeedbackTraceQuery(opts, true)}`, + )) ?? [] + ); +} + +export async function fetchFeedbackTraceBundle( + ctx: ResolvedClientContext, + traceId: string, +): Promise { + const bundle = await ctx.api.get(`/api/feedback-traces/${traceId}/bundle`); + if (!bundle) { + throw new Error(`Feedback trace bundle ${traceId} not found`); + } + return bundle; +} + +export function summarizeFeedbackTraces(traces: FeedbackTrace[]): FeedbackSummary { + const statuses: Record = {}; + let thumbsUp = 0; + let thumbsDown = 0; + let withReason = 0; + + for (const trace of traces) { + if (trace.vote === "up") thumbsUp += 1; + if (trace.vote === "down") thumbsDown += 1; + if (readFeedbackReason(trace)) withReason += 1; + statuses[trace.status] = (statuses[trace.status] ?? 0) + 1; + } + + return { + total: traces.length, + thumbsUp, + thumbsDown, + withReason, + statuses, + }; +} + +export function renderFeedbackReport(input: { + apiBase: string; + companyId: string; + traces: FeedbackTrace[]; + summary: FeedbackSummary; + includePayloads: boolean; +}): string { + const lines: string[] = []; + lines.push(""); + lines.push(pc.bold(pc.magenta("Paperclip Feedback Report"))); + lines.push(pc.dim(new Date().toISOString())); + lines.push(horizontalRule()); + lines.push(`${pc.dim("Server:")} ${input.apiBase}`); + lines.push(`${pc.dim("Company:")} ${input.companyId}`); + lines.push(""); + + if (input.traces.length === 0) { + lines.push(pc.yellow("[!!] No feedback traces found.")); + lines.push(""); + return lines.join("\n"); + } + + lines.push(pc.bold(pc.cyan("Summary"))); + lines.push(horizontalRule()); + lines.push(` ${pc.green(pc.bold(String(input.summary.thumbsUp)))} thumbs up`); + lines.push(` ${pc.red(pc.bold(String(input.summary.thumbsDown)))} thumbs down`); + lines.push(` ${pc.yellow(pc.bold(String(input.summary.withReason)))} downvotes with a reason`); + lines.push(` ${pc.bold(String(input.summary.total))} total traces`); + lines.push(""); + lines.push(pc.dim("Export status:")); + for (const status of ["pending", "sent", "local_only", "failed"]) { + lines.push(` ${padRight(status, 10)} ${input.summary.statuses[status] ?? 0}`); + } + lines.push(""); + lines.push(pc.bold(pc.cyan("Trace Details"))); + lines.push(horizontalRule()); + + for (const trace of input.traces) { + const voteColor = trace.vote === "up" ? pc.green : pc.red; + const voteIcon = trace.vote === "up" ? "^" : "v"; + const issueRef = trace.issueIdentifier ?? trace.issueId; + const label = trace.targetSummary.label?.trim() || trace.targetType; + const excerpt = compactText(trace.targetSummary.excerpt); + const reason = readFeedbackReason(trace); + lines.push( + ` ${voteColor(voteIcon)} ${pc.bold(issueRef)} ${pc.dim(compactText(trace.issueTitle, 64))}`, + ); + lines.push( + ` ${pc.dim("Trace:")} ${trace.id.slice(0, 8)} ${pc.dim("Status:")} ${trace.status} ${pc.dim("Date:")} ${formatTimestamp(trace.createdAt)}`, + ); + lines.push(` ${pc.dim("Target:")} ${label}`); + if (excerpt) { + lines.push(` ${pc.dim("Excerpt:")} ${excerpt}`); + } + if (reason) { + lines.push(` ${pc.yellow(pc.bold("Reason:"))} ${pc.yellow(reason)}`); + } + lines.push(""); + } + + if (input.includePayloads) { + lines.push(pc.bold(pc.cyan("Raw Payloads"))); + lines.push(horizontalRule()); + for (const trace of input.traces) { + if (!trace.payloadSnapshot) continue; + const issueRef = trace.issueIdentifier ?? trace.issueId; + lines.push(` ${pc.bold(`${issueRef} (${trace.id.slice(0, 8)})`)}`); + const body = JSON.stringify(trace.payloadSnapshot, null, 2)?.split("\n") ?? []; + for (const line of body) { + lines.push(` ${pc.dim(line)}`); + } + lines.push(""); + } + } + + lines.push(horizontalRule()); + lines.push(pc.dim(`Report complete. ${input.traces.length} trace(s) displayed.`)); + lines.push(""); + return lines.join("\n"); +} + +export async function writeFeedbackExportBundle(input: { + apiBase: string; + companyId: string; + traces: FeedbackTrace[]; + outputDir: string; + traceBundleFetcher?: (trace: FeedbackTrace) => Promise; +}): Promise { + await ensureEmptyOutputDirectory(input.outputDir); + await mkdir(path.join(input.outputDir, "votes"), { recursive: true }); + await mkdir(path.join(input.outputDir, "traces"), { recursive: true }); + await mkdir(path.join(input.outputDir, "full-traces"), { recursive: true }); + + const summary = summarizeFeedbackTraces(input.traces); + const voteFiles: string[] = []; + const traceFiles: string[] = []; + const fullTraceDirs: string[] = []; + const fullTraceFiles: string[] = []; + const issueSet = new Set(); + + for (const trace of input.traces) { + const issueRef = sanitizeFileSegment(trace.issueIdentifier ?? trace.issueId); + const voteRecord = buildFeedbackVoteRecord(trace); + const voteFileName = `${issueRef}-${trace.feedbackVoteId.slice(0, 8)}.json`; + const traceFileName = `${issueRef}-${trace.id.slice(0, 8)}.json`; + voteFiles.push(voteFileName); + traceFiles.push(traceFileName); + issueSet.add(trace.issueIdentifier ?? trace.issueId); + await writeFile( + path.join(input.outputDir, "votes", voteFileName), + `${JSON.stringify(voteRecord, null, 2)}\n`, + "utf8", + ); + await writeFile( + path.join(input.outputDir, "traces", traceFileName), + `${JSON.stringify(trace, null, 2)}\n`, + "utf8", + ); + + if (input.traceBundleFetcher) { + const bundle = await input.traceBundleFetcher(trace); + const bundleDirName = `${issueRef}-${trace.id.slice(0, 8)}`; + const bundleDir = path.join(input.outputDir, "full-traces", bundleDirName); + await mkdir(bundleDir, { recursive: true }); + fullTraceDirs.push(bundleDirName); + await writeFile( + path.join(bundleDir, "bundle.json"), + `${JSON.stringify(bundle, null, 2)}\n`, + "utf8", + ); + fullTraceFiles.push(path.posix.join("full-traces", bundleDirName, "bundle.json")); + for (const file of bundle.files) { + const targetPath = path.join(bundleDir, file.path); + await mkdir(path.dirname(targetPath), { recursive: true }); + await writeFile(targetPath, file.contents, "utf8"); + fullTraceFiles.push(path.posix.join("full-traces", bundleDirName, file.path.replace(/\\/g, "/"))); + } + } + } + + const zipPath = `${input.outputDir}.zip`; + const manifest: FeedbackExportManifest = { + exportedAt: new Date().toISOString(), + serverUrl: input.apiBase, + companyId: input.companyId, + summary: { + ...summary, + uniqueIssues: issueSet.size, + issues: Array.from(issueSet).sort((left, right) => left.localeCompare(right)), + }, + files: { + votes: voteFiles.slice().sort((left, right) => left.localeCompare(right)), + traces: traceFiles.slice().sort((left, right) => left.localeCompare(right)), + fullTraces: fullTraceDirs.slice().sort((left, right) => left.localeCompare(right)), + zip: path.basename(zipPath), + }, + }; + + await writeFile( + path.join(input.outputDir, "index.json"), + `${JSON.stringify(manifest, null, 2)}\n`, + "utf8", + ); + const archiveFiles = await collectJsonFilesForArchive(input.outputDir, [ + "index.json", + ...manifest.files.votes.map((file) => path.posix.join("votes", file)), + ...manifest.files.traces.map((file) => path.posix.join("traces", file)), + ...fullTraceFiles, + ]); + await writeFile(zipPath, createStoredZipArchive(archiveFiles, path.basename(input.outputDir))); + + return { + outputDir: input.outputDir, + zipPath, + manifest, + }; +} + +export function renderFeedbackExportSummary(exported: FeedbackExportResult): string { + const lines: string[] = []; + lines.push(""); + lines.push(pc.bold(pc.magenta("Paperclip Feedback Export"))); + lines.push(pc.dim(exported.manifest.exportedAt)); + lines.push(horizontalRule()); + lines.push(`${pc.dim("Company:")} ${exported.manifest.companyId}`); + lines.push(`${pc.dim("Output:")} ${exported.outputDir}`); + lines.push(`${pc.dim("Archive:")} ${exported.zipPath}`); + lines.push(""); + lines.push(pc.bold("Export Summary")); + lines.push(horizontalRule()); + lines.push(` ${pc.green(pc.bold(String(exported.manifest.summary.thumbsUp)))} thumbs up`); + lines.push(` ${pc.red(pc.bold(String(exported.manifest.summary.thumbsDown)))} thumbs down`); + lines.push(` ${pc.yellow(pc.bold(String(exported.manifest.summary.withReason)))} with reason`); + lines.push(` ${pc.bold(String(exported.manifest.summary.uniqueIssues))} unique issues`); + lines.push(""); + lines.push(pc.dim("Files:")); + lines.push(` ${path.join(exported.outputDir, "index.json")}`); + lines.push(` ${path.join(exported.outputDir, "votes")} (${exported.manifest.files.votes.length} files)`); + lines.push(` ${path.join(exported.outputDir, "traces")} (${exported.manifest.files.traces.length} files)`); + lines.push(` ${path.join(exported.outputDir, "full-traces")} (${exported.manifest.files.fullTraces.length} bundles)`); + lines.push(` ${exported.zipPath}`); + lines.push(""); + return lines.join("\n"); +} + +function readFeedbackReason(trace: FeedbackTrace): string | null { + const payload = asRecord(trace.payloadSnapshot); + const vote = asRecord(payload?.vote); + const reason = vote?.reason; + return typeof reason === "string" && reason.trim() ? reason.trim() : null; +} + +function buildFeedbackVoteRecord(trace: FeedbackTrace) { + return { + voteId: trace.feedbackVoteId, + traceId: trace.id, + issueId: trace.issueId, + issueIdentifier: trace.issueIdentifier, + issueTitle: trace.issueTitle, + vote: trace.vote, + targetType: trace.targetType, + targetId: trace.targetId, + targetSummary: trace.targetSummary, + status: trace.status, + consentVersion: trace.consentVersion, + createdAt: trace.createdAt, + updatedAt: trace.updatedAt, + reason: readFeedbackReason(trace), + }; +} + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; +} + +function compactText(value: string | null | undefined, maxLength = 88): string | null { + if (!value) return null; + const compact = value.replace(/\s+/g, " ").trim(); + if (!compact) return null; + if (compact.length <= maxLength) return compact; + return `${compact.slice(0, maxLength - 3)}...`; +} + +function formatTimestamp(value: unknown): string { + if (value instanceof Date) return value.toISOString().slice(0, 19).replace("T", " "); + if (typeof value === "string") return value.slice(0, 19).replace("T", " "); + return "-"; +} + +function horizontalRule(): string { + return pc.dim("-".repeat(72)); +} + +function padRight(value: string, width: number): string { + return `${value}${" ".repeat(Math.max(0, width - value.length))}`; +} + +function defaultFeedbackExportDirName(): string { + const iso = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); + return `feedback-export-${iso}`; +} + +async function ensureEmptyOutputDirectory(outputDir: string): Promise { + try { + const info = await stat(outputDir); + if (!info.isDirectory()) { + throw new Error(`Output path already exists and is not a directory: ${outputDir}`); + } + const entries = await readdir(outputDir); + if (entries.length > 0) { + throw new Error(`Output directory already exists and is not empty: ${outputDir}`); + } + } catch (error) { + const message = error instanceof Error ? error.message : ""; + if (/ENOENT/.test(message)) { + await mkdir(outputDir, { recursive: true }); + return; + } + throw error; + } +} + +async function collectJsonFilesForArchive( + outputDir: string, + relativePaths: string[], +): Promise> { + const files: Record = {}; + for (const relativePath of relativePaths) { + const normalized = relativePath.replace(/\\/g, "/"); + files[normalized] = await readFile(path.join(outputDir, normalized), "utf8"); + } + return files; +} + +function sanitizeFileSegment(value: string): string { + return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "feedback"; +} + +function writeUint16(target: Uint8Array, offset: number, value: number) { + target[offset] = value & 0xff; + target[offset + 1] = (value >>> 8) & 0xff; +} + +function writeUint32(target: Uint8Array, offset: number, value: number) { + target[offset] = value & 0xff; + target[offset + 1] = (value >>> 8) & 0xff; + target[offset + 2] = (value >>> 16) & 0xff; + target[offset + 3] = (value >>> 24) & 0xff; +} + +function crc32(bytes: Uint8Array) { + let crc = 0xffffffff; + for (const byte of bytes) { + crc ^= byte; + for (let bit = 0; bit < 8; bit += 1) { + crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1; + } + } + return (crc ^ 0xffffffff) >>> 0; +} + +function createStoredZipArchive(files: Record, rootPath: string): Uint8Array { + const encoder = new TextEncoder(); + const localChunks: Uint8Array[] = []; + const centralChunks: Uint8Array[] = []; + let localOffset = 0; + let entryCount = 0; + + for (const [relativePath, content] of Object.entries(files).sort(([left], [right]) => left.localeCompare(right))) { + const fileName = encoder.encode(`${rootPath}/${relativePath}`); + const body = encoder.encode(content); + const checksum = crc32(body); + + const localHeader = new Uint8Array(30 + fileName.length); + writeUint32(localHeader, 0, 0x04034b50); + writeUint16(localHeader, 4, 20); + writeUint16(localHeader, 6, 0x0800); + writeUint16(localHeader, 8, 0); + writeUint32(localHeader, 14, checksum); + writeUint32(localHeader, 18, body.length); + writeUint32(localHeader, 22, body.length); + writeUint16(localHeader, 26, fileName.length); + localHeader.set(fileName, 30); + + const centralHeader = new Uint8Array(46 + fileName.length); + writeUint32(centralHeader, 0, 0x02014b50); + writeUint16(centralHeader, 4, 20); + writeUint16(centralHeader, 6, 20); + writeUint16(centralHeader, 8, 0x0800); + writeUint16(centralHeader, 10, 0); + writeUint32(centralHeader, 16, checksum); + writeUint32(centralHeader, 20, body.length); + writeUint32(centralHeader, 24, body.length); + writeUint16(centralHeader, 28, fileName.length); + writeUint32(centralHeader, 42, localOffset); + centralHeader.set(fileName, 46); + + localChunks.push(localHeader, body); + centralChunks.push(centralHeader); + localOffset += localHeader.length + body.length; + entryCount += 1; + } + + const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0); + const archive = new Uint8Array( + localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + centralDirectoryLength + 22, + ); + let offset = 0; + for (const chunk of localChunks) { + archive.set(chunk, offset); + offset += chunk.length; + } + const centralDirectoryOffset = offset; + for (const chunk of centralChunks) { + archive.set(chunk, offset); + offset += chunk.length; + } + writeUint32(archive, offset, 0x06054b50); + writeUint16(archive, offset + 8, entryCount); + writeUint16(archive, offset + 10, entryCount); + writeUint32(archive, offset + 12, centralDirectoryLength); + writeUint32(archive, offset + 16, centralDirectoryOffset); + return archive; +} diff --git a/cli/src/commands/client/issue.ts b/cli/src/commands/client/issue.ts index 8db617d9..c3517dd1 100644 --- a/cli/src/commands/client/issue.ts +++ b/cli/src/commands/client/issue.ts @@ -1,8 +1,10 @@ import { Command } from "commander"; +import { writeFile } from "node:fs/promises"; import { addIssueCommentSchema, checkoutIssueSchema, createIssueSchema, + type FeedbackTrace, updateIssueSchema, type Issue, type IssueComment, @@ -61,6 +63,18 @@ interface IssueCheckoutOptions extends BaseClientOptions { expectedStatuses?: string; } +interface IssueFeedbackOptions extends BaseClientOptions { + targetType?: string; + vote?: string; + status?: string; + from?: string; + to?: string; + sharedOnly?: boolean; + includePayload?: boolean; + out?: string; + format?: string; +} + export function registerIssueCommands(program: Command): void { const issue = program.command("issue").description("Issue operations"); @@ -237,6 +251,85 @@ export function registerIssueCommands(program: Command): void { }), ); + addCommonClientOptions( + issue + .command("feedback:list") + .description("List feedback traces for an issue") + .argument("", "Issue ID") + .option("--target-type ", "Filter by target type") + .option("--vote ", "Filter by vote value") + .option("--status ", "Filter by trace status") + .option("--from ", "Only include traces created at or after this timestamp") + .option("--to ", "Only include traces created at or before this timestamp") + .option("--shared-only", "Only include traces eligible for sharing/export") + .option("--include-payload", "Include stored payload snapshots in the response") + .action(async (issueId: string, opts: IssueFeedbackOptions) => { + try { + const ctx = resolveCommandContext(opts); + const traces = (await ctx.api.get( + `/api/issues/${issueId}/feedback-traces${buildFeedbackTraceQuery(opts)}`, + )) ?? []; + if (ctx.json) { + printOutput(traces, { json: true }); + return; + } + printOutput( + traces.map((trace) => ({ + id: trace.id, + issue: trace.issueIdentifier ?? trace.issueId, + vote: trace.vote, + status: trace.status, + targetType: trace.targetType, + target: trace.targetSummary.label, + })), + { json: false }, + ); + } catch (err) { + handleCommandError(err); + } + }), + ); + + addCommonClientOptions( + issue + .command("feedback:export") + .description("Export feedback traces for an issue") + .argument("", "Issue ID") + .option("--target-type ", "Filter by target type") + .option("--vote ", "Filter by vote value") + .option("--status ", "Filter by trace status") + .option("--from ", "Only include traces created at or after this timestamp") + .option("--to ", "Only include traces created at or before this timestamp") + .option("--shared-only", "Only include traces eligible for sharing/export") + .option("--include-payload", "Include stored payload snapshots in the export") + .option("--out ", "Write export to a file path instead of stdout") + .option("--format ", "Export format: json or ndjson", "ndjson") + .action(async (issueId: string, opts: IssueFeedbackOptions) => { + try { + const ctx = resolveCommandContext(opts); + const traces = (await ctx.api.get( + `/api/issues/${issueId}/feedback-traces${buildFeedbackTraceQuery({ + ...opts, + includePayload: opts.includePayload ?? true, + })}`, + )) ?? []; + const serialized = serializeFeedbackTraces(traces, opts.format); + if (opts.out?.trim()) { + await writeFile(opts.out, serialized, "utf8"); + if (ctx.json) { + printOutput({ out: opts.out, count: traces.length, format: normalizeExportFormat(opts.format) }, { json: true }); + return; + } + console.log(`Wrote ${traces.length} feedback trace(s) to ${opts.out}`); + return; + } + process.stdout.write(`${serialized}${serialized.endsWith("\n") ? "" : "\n"}`); + } catch (err) { + handleCommandError(err); + } + }), + ); + addCommonClientOptions( issue .command("checkout") @@ -311,3 +404,29 @@ function filterIssueRows(rows: Issue[], match: string | undefined): Issue[] { return text.includes(needle); }); } + +function buildFeedbackTraceQuery(opts: IssueFeedbackOptions): string { + const params = new URLSearchParams(); + if (opts.targetType) params.set("targetType", opts.targetType); + if (opts.vote) params.set("vote", opts.vote); + if (opts.status) params.set("status", opts.status); + if (opts.from) params.set("from", opts.from); + if (opts.to) params.set("to", opts.to); + if (opts.sharedOnly) params.set("sharedOnly", "true"); + if (opts.includePayload) params.set("includePayload", "true"); + const query = params.toString(); + return query ? `?${query}` : ""; +} + +function normalizeExportFormat(value: string | undefined): "json" | "ndjson" { + if (!value || value === "ndjson") return "ndjson"; + if (value === "json") return "json"; + throw new Error(`Unsupported export format: ${value}`); +} + +function serializeFeedbackTraces(traces: FeedbackTrace[], format: string | undefined): string { + if (normalizeExportFormat(format) === "json") { + return JSON.stringify(traces, null, 2); + } + return traces.map((trace) => JSON.stringify(trace)).join("\n"); +} diff --git a/cli/src/index.ts b/cli/src/index.ts index 828404e8..c4e1655e 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -15,6 +15,7 @@ import { registerAgentCommands } from "./commands/client/agent.js"; import { registerApprovalCommands } from "./commands/client/approval.js"; import { registerActivityCommands } from "./commands/client/activity.js"; import { registerDashboardCommands } from "./commands/client/dashboard.js"; +import { registerFeedbackCommands } from "./commands/client/feedback.js"; import { applyDataDirOverride, type DataDirOptionLike } from "./config/data-dir.js"; import { loadPaperclipEnvFile } from "./config/env.js"; import { registerWorktreeCommands } from "./commands/worktree.js"; @@ -137,6 +138,7 @@ registerAgentCommands(program); registerApprovalCommands(program); registerActivityCommands(program); registerDashboardCommands(program); +registerFeedbackCommands(program); registerWorktreeCommands(program); registerPluginCommands(program); diff --git a/docs/feedback-voting.md b/docs/feedback-voting.md new file mode 100644 index 00000000..2f754bce --- /dev/null +++ b/docs/feedback-voting.md @@ -0,0 +1,189 @@ +# Feedback Voting — Local Data Guide + +When you rate an agent's response with **Helpful** (thumbs up) or **Needs work** (thumbs down), Paperclip saves your vote locally alongside your running instance. This guide covers what gets stored, how to access it, and how to export it. + +## How voting works + +1. Click **Helpful** or **Needs work** on any agent comment or document revision. +2. If you click **Needs work**, an optional text prompt appears: _"What could have been better?"_ You can type a reason or dismiss it. +3. A consent dialog asks whether to keep the vote local or share it. Your choice is remembered for future votes. + +### What gets stored + +Each vote creates two local records: + +| Record | What it contains | +|--------|-----------------| +| **Vote** | Your vote (up/down), optional reason text, sharing preference, consent version, timestamp | +| **Trace bundle** | Full context snapshot: the voted-on comment/revision text, issue title, agent info, your vote, and reason — everything needed to understand the feedback in isolation | + +All data lives in your local Paperclip database. Nothing leaves your machine unless you explicitly choose to share. + +When a vote is marked for sharing, Paperclip also queues the trace bundle for background export through the Telemetry Backend. The app server never uploads raw feedback trace bundles directly to object storage. + +## Viewing your votes + +### Quick report (terminal) + +```bash +pnpm paperclipai feedback report +``` + +Shows a color-coded summary: vote counts, per-trace details with reasons, and export statuses. + +```bash +# Installed CLI +paperclipai feedback report + +# Point to a different server or company +pnpm paperclipai feedback report --api-base http://127.0.0.1:3000 --company-id + +# Include raw payload dumps in the report +pnpm paperclipai feedback report --payloads +``` + +### API endpoints + +All endpoints require board-user access (automatic in local dev). + +**List votes for an issue:** +```bash +curl http://127.0.0.1:3102/api/issues//feedback-votes +``` + +**List trace bundles for an issue (with full payloads):** +```bash +curl 'http://127.0.0.1:3102/api/issues//feedback-traces?includePayload=true' +``` + +**List all traces company-wide:** +```bash +curl 'http://127.0.0.1:3102/api/companies//feedback-traces?includePayload=true' +``` + +**Get a single trace envelope record:** +```bash +curl http://127.0.0.1:3102/api/feedback-traces/ +``` + +**Get the full export bundle for a trace:** +```bash +curl http://127.0.0.1:3102/api/feedback-traces//bundle +``` + +#### Filtering + +The trace endpoints accept query parameters: + +| Parameter | Values | Description | +|-----------|--------|-------------| +| `vote` | `up`, `down` | Filter by vote direction | +| `status` | `local_only`, `pending`, `sent`, `failed` | Filter by export status | +| `targetType` | `issue_comment`, `issue_document_revision` | Filter by what was voted on | +| `sharedOnly` | `true` | Only show votes the user chose to share | +| `includePayload` | `true` | Include the full context snapshot | +| `from` / `to` | ISO date | Date range filter | + +## Exporting your data + +### Export to files + zip + +```bash +pnpm paperclipai feedback export +``` + +Creates a timestamped directory with: + +``` +feedback-export-20260331T120000Z/ + index.json # manifest with summary stats + votes/ + PAP-123-a1b2c3d4.json # vote metadata (one per vote) + traces/ + PAP-123-e5f6g7h8.json # Paperclip feedback envelope (one per trace) + full-traces/ + PAP-123-e5f6g7h8/ + bundle.json # full export manifest for the trace + ...raw adapter files # codex / claude / opencode session artifacts when available +feedback-export-20260331T120000Z.zip +``` + +Exports are full by default. `traces/` keeps the Paperclip envelope, while `full-traces/` contains the richer per-trace bundle plus any recoverable adapter-native files. + +```bash +# Custom server and output directory +pnpm paperclipai feedback export --api-base http://127.0.0.1:3000 --company-id --out ./my-export +``` + +### Reading an exported trace + +Open any file in `traces/` to see: + +```json +{ + "id": "trace-uuid", + "vote": "down", + "issueIdentifier": "PAP-123", + "issueTitle": "Fix login timeout", + "targetType": "issue_comment", + "targetSummary": { + "label": "Comment", + "excerpt": "The first 80 chars of the comment that was voted on..." + }, + "payloadSnapshot": { + "vote": { + "value": "down", + "reason": "Did not address the root cause" + }, + "target": { + "body": "Full text of the agent comment..." + }, + "issue": { + "identifier": "PAP-123", + "title": "Fix login timeout" + } + } +} +``` + +Open `full-traces/-/bundle.json` to see the expanded export metadata, including capture notes, adapter type, integrity metadata, and the inventory of raw files written alongside it. + +Built-in local adapters now export their native session artifacts more directly: + +- `codex_local`: `adapter/codex/session.jsonl` +- `claude_local`: `adapter/claude/session.jsonl`, plus any `adapter/claude/session/...` sidecar files and `adapter/claude/debug.txt` when present +- `opencode_local`: `adapter/opencode/session.json`, `adapter/opencode/messages/*.json`, and `adapter/opencode/parts//*.json`, with optional `project.json`, `todo.json`, and `session-diff.json` + +## Sharing preferences + +The first time you vote, a consent dialog asks: + +- **Keep local** — vote is stored locally only (`sharedWithLabs: false`) +- **Share this vote** — vote is marked for sharing (`sharedWithLabs: true`) + +Your preference is saved per-company. You can change it any time via the feedback settings. Votes marked "keep local" are never queued for export. + +## Data lifecycle + +| Status | Meaning | +|--------|---------| +| `local_only` | Vote stored locally, not marked for sharing | +| `pending` | Marked for sharing, waiting to be sent | +| `sent` | Successfully transmitted | +| `failed` | Transmission attempted but failed (will retry) | + +Your local database always retains the full vote and trace data regardless of sharing status. + +## Remote sync + +Votes you choose to share are queued as `pending` traces and flushed by the server's background worker to the Telemetry Backend. The Telemetry Backend validates the request, then persists the bundle into its configured object storage. + +- App server responsibility: build the bundle, POST it to Telemetry Backend, update trace status +- Telemetry Backend responsibility: authenticate the request, validate payload shape, compress/store the bundle, return the final object key +- Retry behavior: failed uploads move to `failed` with an error message in `failureReason`, and the worker retries them on later ticks + +Exported objects use a deterministic key pattern so they are easy to inspect: + +```text +feedback-traces//YYYY/MM/DD/.json +``` diff --git a/packages/db/src/migrations/0047_overjoyed_groot.sql b/packages/db/src/migrations/0047_overjoyed_groot.sql new file mode 100644 index 00000000..2185200e --- /dev/null +++ b/packages/db/src/migrations/0047_overjoyed_groot.sql @@ -0,0 +1,70 @@ +CREATE TABLE "feedback_exports" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "feedback_vote_id" uuid NOT NULL, + "issue_id" uuid NOT NULL, + "project_id" uuid, + "author_user_id" text NOT NULL, + "target_type" text NOT NULL, + "target_id" text NOT NULL, + "vote" text NOT NULL, + "status" text DEFAULT 'local_only' NOT NULL, + "destination" text, + "export_id" text, + "consent_version" text, + "schema_version" text DEFAULT 'paperclip-feedback-envelope-v2' NOT NULL, + "bundle_version" text DEFAULT 'paperclip-feedback-bundle-v2' NOT NULL, + "payload_version" text DEFAULT 'paperclip-feedback-v1' NOT NULL, + "payload_digest" text, + "payload_snapshot" jsonb, + "target_summary" jsonb NOT NULL, + "redaction_summary" jsonb, + "attempt_count" integer DEFAULT 0 NOT NULL, + "last_attempted_at" timestamp with time zone, + "exported_at" timestamp with time zone, + "failure_reason" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "feedback_votes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "issue_id" uuid NOT NULL, + "target_type" text NOT NULL, + "target_id" text NOT NULL, + "author_user_id" text NOT NULL, + "vote" text NOT NULL, + "reason" text, + "shared_with_labs" boolean DEFAULT false NOT NULL, + "shared_at" timestamp with time zone, + "consent_version" text, + "redaction_summary" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "companies" ADD COLUMN "feedback_data_sharing_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "companies" ADD COLUMN "feedback_data_sharing_consent_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "companies" ADD COLUMN "feedback_data_sharing_consent_by_user_id" text;--> statement-breakpoint +ALTER TABLE "companies" ADD COLUMN "feedback_data_sharing_terms_version" text;--> statement-breakpoint +ALTER TABLE "document_revisions" ADD COLUMN "created_by_run_id" uuid;--> statement-breakpoint +ALTER TABLE "issue_comments" ADD COLUMN "created_by_run_id" uuid;--> statement-breakpoint +ALTER TABLE "feedback_exports" ADD CONSTRAINT "feedback_exports_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "feedback_exports" ADD CONSTRAINT "feedback_exports_feedback_vote_id_feedback_votes_id_fk" FOREIGN KEY ("feedback_vote_id") REFERENCES "public"."feedback_votes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "feedback_exports" ADD CONSTRAINT "feedback_exports_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "feedback_exports" ADD CONSTRAINT "feedback_exports_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "feedback_votes" ADD CONSTRAINT "feedback_votes_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "feedback_votes" ADD CONSTRAINT "feedback_votes_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "feedback_exports_feedback_vote_idx" ON "feedback_exports" USING btree ("feedback_vote_id");--> statement-breakpoint +CREATE INDEX "feedback_exports_company_created_idx" ON "feedback_exports" USING btree ("company_id","created_at");--> statement-breakpoint +CREATE INDEX "feedback_exports_company_status_idx" ON "feedback_exports" USING btree ("company_id","status","created_at");--> statement-breakpoint +CREATE INDEX "feedback_exports_company_issue_idx" ON "feedback_exports" USING btree ("company_id","issue_id","created_at");--> statement-breakpoint +CREATE INDEX "feedback_exports_company_project_idx" ON "feedback_exports" USING btree ("company_id","project_id","created_at");--> statement-breakpoint +CREATE INDEX "feedback_exports_company_author_idx" ON "feedback_exports" USING btree ("company_id","author_user_id","created_at");--> statement-breakpoint +CREATE INDEX "feedback_votes_company_issue_idx" ON "feedback_votes" USING btree ("company_id","issue_id");--> statement-breakpoint +CREATE INDEX "feedback_votes_issue_target_idx" ON "feedback_votes" USING btree ("issue_id","target_type","target_id");--> statement-breakpoint +CREATE INDEX "feedback_votes_author_idx" ON "feedback_votes" USING btree ("author_user_id","created_at");--> statement-breakpoint +CREATE UNIQUE INDEX "feedback_votes_company_target_author_idx" ON "feedback_votes" USING btree ("company_id","target_type","target_id","author_user_id");--> statement-breakpoint +ALTER TABLE "document_revisions" ADD CONSTRAINT "document_revisions_created_by_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("created_by_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issue_comments" ADD CONSTRAINT "issue_comments_created_by_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("created_by_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0047_snapshot.json b/packages/db/src/migrations/meta/0047_snapshot.json new file mode 100644 index 00000000..266ecf37 --- /dev/null +++ b/packages/db/src/migrations/meta/0047_snapshot.json @@ -0,0 +1,12539 @@ +{ + "id": "a0553dd4-5278-406f-bef7-b06597f92cf2", + "prevId": "4ae31a44-1b98-4437-88ef-92b76d014107", + "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.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_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 + }, + "context_snapshot": { + "name": "context_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "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.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": {} + } + }, + "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_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_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_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_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": {} + } + }, + "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 + }, + "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": true + }, + "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": true + }, + "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'" + }, + "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.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 adb00a47..cca7597b 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -330,6 +330,13 @@ "when": 1774960197878, "tag": "0046_smooth_sentinels", "breakpoints": true + }, + { + "idx": 47, + "version": "7", + "when": 1775137972687, + "tag": "0047_overjoyed_groot", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/companies.ts b/packages/db/src/schema/companies.ts index 83d6e193..f8566d27 100644 --- a/packages/db/src/schema/companies.ts +++ b/packages/db/src/schema/companies.ts @@ -16,6 +16,12 @@ export const companies = pgTable( requireBoardApprovalForNewAgents: boolean("require_board_approval_for_new_agents") .notNull() .default(true), + feedbackDataSharingEnabled: boolean("feedback_data_sharing_enabled") + .notNull() + .default(false), + feedbackDataSharingConsentAt: timestamp("feedback_data_sharing_consent_at", { withTimezone: true }), + feedbackDataSharingConsentByUserId: text("feedback_data_sharing_consent_by_user_id"), + feedbackDataSharingTermsVersion: text("feedback_data_sharing_terms_version"), brandColor: text("brand_color"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), diff --git a/packages/db/src/schema/document_revisions.ts b/packages/db/src/schema/document_revisions.ts index 17eceb1e..cdc8a18f 100644 --- a/packages/db/src/schema/document_revisions.ts +++ b/packages/db/src/schema/document_revisions.ts @@ -2,6 +2,7 @@ import { pgTable, uuid, text, integer, timestamp, index, uniqueIndex } from "dri import { companies } from "./companies.js"; import { agents } from "./agents.js"; import { documents } from "./documents.js"; +import { heartbeatRuns } from "./heartbeat_runs.js"; export const documentRevisions = pgTable( "document_revisions", @@ -16,6 +17,7 @@ export const documentRevisions = pgTable( changeSummary: text("change_summary"), createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }), createdByUserId: text("created_by_user_id"), + createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ diff --git a/packages/db/src/schema/feedback_exports.ts b/packages/db/src/schema/feedback_exports.ts new file mode 100644 index 00000000..e4b7f174 --- /dev/null +++ b/packages/db/src/schema/feedback_exports.ts @@ -0,0 +1,45 @@ +import { index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { feedbackVotes } from "./feedback_votes.js"; +import { issues } from "./issues.js"; +import { projects } from "./projects.js"; + +export const feedbackExports = pgTable( + "feedback_exports", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + feedbackVoteId: uuid("feedback_vote_id").notNull().references(() => feedbackVotes.id, { onDelete: "cascade" }), + issueId: uuid("issue_id").notNull().references(() => issues.id, { onDelete: "cascade" }), + projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }), + authorUserId: text("author_user_id").notNull(), + targetType: text("target_type").notNull(), + targetId: text("target_id").notNull(), + vote: text("vote").notNull(), + status: text("status").notNull().default("local_only"), + destination: text("destination"), + exportId: text("export_id"), + consentVersion: text("consent_version"), + schemaVersion: text("schema_version").notNull().default("paperclip-feedback-envelope-v2"), + bundleVersion: text("bundle_version").notNull().default("paperclip-feedback-bundle-v2"), + payloadVersion: text("payload_version").notNull().default("paperclip-feedback-v1"), + payloadDigest: text("payload_digest"), + payloadSnapshot: jsonb("payload_snapshot"), + targetSummary: jsonb("target_summary").notNull(), + redactionSummary: jsonb("redaction_summary"), + attemptCount: integer("attempt_count").notNull().default(0), + lastAttemptedAt: timestamp("last_attempted_at", { withTimezone: true }), + exportedAt: timestamp("exported_at", { withTimezone: true }), + failureReason: text("failure_reason"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + voteUniqueIdx: uniqueIndex("feedback_exports_feedback_vote_idx").on(table.feedbackVoteId), + companyCreatedIdx: index("feedback_exports_company_created_idx").on(table.companyId, table.createdAt), + companyStatusIdx: index("feedback_exports_company_status_idx").on(table.companyId, table.status, table.createdAt), + companyIssueIdx: index("feedback_exports_company_issue_idx").on(table.companyId, table.issueId, table.createdAt), + companyProjectIdx: index("feedback_exports_company_project_idx").on(table.companyId, table.projectId, table.createdAt), + companyAuthorIdx: index("feedback_exports_company_author_idx").on(table.companyId, table.authorUserId, table.createdAt), + }), +); diff --git a/packages/db/src/schema/feedback_votes.ts b/packages/db/src/schema/feedback_votes.ts new file mode 100644 index 00000000..c45f1650 --- /dev/null +++ b/packages/db/src/schema/feedback_votes.ts @@ -0,0 +1,34 @@ +import { boolean, index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { issues } from "./issues.js"; + +export const feedbackVotes = pgTable( + "feedback_votes", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + issueId: uuid("issue_id").notNull().references(() => issues.id), + targetType: text("target_type").notNull(), + targetId: text("target_id").notNull(), + authorUserId: text("author_user_id").notNull(), + vote: text("vote").notNull(), + reason: text("reason"), + sharedWithLabs: boolean("shared_with_labs").notNull().default(false), + sharedAt: timestamp("shared_at", { withTimezone: true }), + consentVersion: text("consent_version"), + redactionSummary: jsonb("redaction_summary"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyIssueIdx: index("feedback_votes_company_issue_idx").on(table.companyId, table.issueId), + issueTargetIdx: index("feedback_votes_issue_target_idx").on(table.issueId, table.targetType, table.targetId), + authorIdx: index("feedback_votes_author_idx").on(table.authorUserId, table.createdAt), + companyTargetAuthorUniqueIdx: uniqueIndex("feedback_votes_company_target_author_idx").on( + table.companyId, + table.targetType, + table.targetId, + table.authorUserId, + ), + }), +); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index fc387334..1b6fe01f 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -32,6 +32,8 @@ export { issueLabels } from "./issue_labels.js"; export { issueApprovals } from "./issue_approvals.js"; export { issueComments } from "./issue_comments.js"; export { issueInboxArchives } from "./issue_inbox_archives.js"; +export { feedbackVotes } from "./feedback_votes.js"; +export { feedbackExports } from "./feedback_exports.js"; export { issueReadStates } from "./issue_read_states.js"; export { assets } from "./assets.js"; export { issueAttachments } from "./issue_attachments.js"; diff --git a/packages/db/src/schema/issue_comments.ts b/packages/db/src/schema/issue_comments.ts index fcefb830..9a557ca2 100644 --- a/packages/db/src/schema/issue_comments.ts +++ b/packages/db/src/schema/issue_comments.ts @@ -2,6 +2,7 @@ import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core"; import { companies } from "./companies.js"; import { issues } from "./issues.js"; import { agents } from "./agents.js"; +import { heartbeatRuns } from "./heartbeat_runs.js"; export const issueComments = pgTable( "issue_comments", @@ -11,6 +12,7 @@ export const issueComments = pgTable( issueId: uuid("issue_id").notNull().references(() => issues.id), authorAgentId: uuid("author_agent_id").references(() => agents.id), authorUserId: text("author_user_id"), + createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }), body: text("body").notNull(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index f75dbf14..2e997ef3 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -138,6 +138,16 @@ export { export type { Company, + FeedbackVote, + FeedbackDataSharingPreference, + FeedbackTargetType, + FeedbackVoteValue, + FeedbackTrace, + FeedbackTraceStatus, + FeedbackTraceTargetSummary, + FeedbackTraceBundleCaptureStatus, + FeedbackTraceBundleFile, + FeedbackTraceBundle, CompanySkillSourceType, CompanySkillTrustLevel, CompanySkillCompatibility, @@ -325,6 +335,15 @@ export type { ProviderQuotaResult, } from "./types/index.js"; +export { + DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, + FEEDBACK_TARGET_TYPES, + FEEDBACK_DATA_SHARING_PREFERENCES, + FEEDBACK_TRACE_STATUSES, + FEEDBACK_VOTE_VALUES, + DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION, +} from "./types/feedback.js"; + export { instanceGeneralSettingsSchema, patchInstanceGeneralSettingsSchema, @@ -338,9 +357,14 @@ export { createCompanySchema, updateCompanySchema, updateCompanyBrandingSchema, + feedbackTargetTypeSchema, + feedbackTraceStatusSchema, + feedbackVoteValueSchema, + upsertIssueFeedbackVoteSchema, type CreateCompany, type UpdateCompany, type UpdateCompanyBranding, + type UpsertIssueFeedbackVote, agentSkillStateSchema, agentSkillSyncModeSchema, agentSkillEntrySchema, diff --git a/packages/shared/src/types/company-portability.ts b/packages/shared/src/types/company-portability.ts index 63016e93..2649e03a 100644 --- a/packages/shared/src/types/company-portability.ts +++ b/packages/shared/src/types/company-portability.ts @@ -31,6 +31,10 @@ export interface CompanyPortabilityCompanyManifestEntry { brandColor: string | null; logoPath: string | null; requireBoardApprovalForNewAgents: boolean; + feedbackDataSharingEnabled: boolean; + feedbackDataSharingConsentAt: string | null; + feedbackDataSharingConsentByUserId: string | null; + feedbackDataSharingTermsVersion: string | null; } export interface CompanyPortabilitySidebarOrder { diff --git a/packages/shared/src/types/company.ts b/packages/shared/src/types/company.ts index 9f6d3168..bd706c18 100644 --- a/packages/shared/src/types/company.ts +++ b/packages/shared/src/types/company.ts @@ -12,6 +12,10 @@ export interface Company { budgetMonthlyCents: number; spentMonthlyCents: number; requireBoardApprovalForNewAgents: boolean; + feedbackDataSharingEnabled: boolean; + feedbackDataSharingConsentAt: Date | null; + feedbackDataSharingConsentByUserId: string | null; + feedbackDataSharingTermsVersion: string | null; brandColor: string | null; logoAssetId: string | null; logoUrl: string | null; diff --git a/packages/shared/src/types/feedback.ts b/packages/shared/src/types/feedback.ts new file mode 100644 index 00000000..c5eb3154 --- /dev/null +++ b/packages/shared/src/types/feedback.ts @@ -0,0 +1,120 @@ +export const FEEDBACK_TARGET_TYPES = ["issue_comment", "issue_document_revision"] as const; +export type FeedbackTargetType = (typeof FEEDBACK_TARGET_TYPES)[number]; + +export const FEEDBACK_VOTE_VALUES = ["up", "down"] as const; +export type FeedbackVoteValue = (typeof FEEDBACK_VOTE_VALUES)[number]; + +export const FEEDBACK_DATA_SHARING_PREFERENCES = ["allowed", "not_allowed", "prompt"] as const; +export type FeedbackDataSharingPreference = (typeof FEEDBACK_DATA_SHARING_PREFERENCES)[number]; + +export const DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE: FeedbackDataSharingPreference = "prompt"; + +export const FEEDBACK_TRACE_STATUSES = ["local_only", "pending", "sent", "failed"] as const; +export type FeedbackTraceStatus = (typeof FEEDBACK_TRACE_STATUSES)[number]; + +export const DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION = "feedback-data-sharing-v1"; + +export interface FeedbackVote { + id: string; + companyId: string; + issueId: string; + targetType: FeedbackTargetType; + targetId: string; + authorUserId: string; + vote: FeedbackVoteValue; + reason: string | null; + sharedWithLabs: boolean; + sharedAt: Date | null; + consentVersion: string | null; + redactionSummary: Record | null; + createdAt: Date; + updatedAt: Date; +} + +export interface FeedbackTraceTargetSummary { + label: string; + excerpt: string | null; + authorAgentId: string | null; + authorUserId: string | null; + createdAt: Date | null; + documentKey: string | null; + documentTitle: string | null; + revisionNumber: number | null; +} + +export interface FeedbackTrace { + id: string; + companyId: string; + feedbackVoteId: string; + issueId: string; + projectId: string | null; + issueIdentifier: string | null; + issueTitle: string; + authorUserId: string; + targetType: FeedbackTargetType; + targetId: string; + vote: FeedbackVoteValue; + status: FeedbackTraceStatus; + destination: string | null; + exportId: string | null; + consentVersion: string | null; + schemaVersion: string; + bundleVersion: string; + payloadVersion: string; + payloadDigest: string | null; + payloadSnapshot: Record | null; + targetSummary: FeedbackTraceTargetSummary; + redactionSummary: Record | null; + attemptCount: number; + lastAttemptedAt: Date | null; + exportedAt: Date | null; + failureReason: string | null; + createdAt: Date; + updatedAt: Date; +} + +export type FeedbackTraceBundleCaptureStatus = "full" | "partial" | "unavailable"; + +export interface FeedbackTraceBundleFile { + path: string; + contentType: string; + encoding: "utf8"; + byteLength: number; + sha256: string; + source: + | "paperclip_run" + | "paperclip_run_events" + | "paperclip_run_log" + | "codex_session" + | "claude_stream_json" + | "claude_project_session" + | "claude_project_artifact" + | "claude_debug_log" + | "claude_task_metadata" + | "opencode_session" + | "opencode_session_diff" + | "opencode_message" + | "opencode_message_part" + | "opencode_project" + | "opencode_todo"; + contents: string; +} + +export interface FeedbackTraceBundle { + traceId: string; + exportId: string | null; + companyId: string; + issueId: string; + issueIdentifier: string | null; + adapterType: string | null; + captureStatus: FeedbackTraceBundleCaptureStatus; + notes: string[]; + envelope: Record; + surface: Record | null; + paperclipRun: Record | null; + rawAdapterTrace: Record | null; + normalizedAdapterTrace: Record | null; + privacy: Record | null; + integrity: Record; + files: FeedbackTraceBundleFile[]; +} diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index dfe4b9d5..d76b37a8 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -1,4 +1,16 @@ export type { Company } from "./company.js"; +export type { + FeedbackVote, + FeedbackDataSharingPreference, + FeedbackTargetType, + FeedbackVoteValue, + FeedbackTrace, + FeedbackTraceStatus, + FeedbackTraceTargetSummary, + FeedbackTraceBundleCaptureStatus, + FeedbackTraceBundleFile, + FeedbackTraceBundle, +} from "./feedback.js"; export type { InstanceExperimentalSettings, InstanceGeneralSettings, InstanceSettings } from "./instance.js"; export type { CompanySkillSourceType, diff --git a/packages/shared/src/types/instance.ts b/packages/shared/src/types/instance.ts index 562c55b3..ec156d89 100644 --- a/packages/shared/src/types/instance.ts +++ b/packages/shared/src/types/instance.ts @@ -1,5 +1,8 @@ +import type { FeedbackDataSharingPreference } from "./feedback.js"; + export interface InstanceGeneralSettings { censorUsernameInLogs: boolean; + feedbackDataSharingPreference: FeedbackDataSharingPreference; } export interface InstanceExperimentalSettings { diff --git a/packages/shared/src/validators/company-portability.ts b/packages/shared/src/validators/company-portability.ts index 7cbd4884..8a3a4545 100644 --- a/packages/shared/src/validators/company-portability.ts +++ b/packages/shared/src/validators/company-portability.ts @@ -36,6 +36,10 @@ export const portabilityCompanyManifestEntrySchema = z.object({ brandColor: z.string().nullable(), logoPath: z.string().nullable(), requireBoardApprovalForNewAgents: z.boolean(), + feedbackDataSharingEnabled: z.boolean().default(false), + feedbackDataSharingConsentAt: z.string().datetime().nullable().default(null), + feedbackDataSharingConsentByUserId: z.string().nullable().default(null), + feedbackDataSharingTermsVersion: z.string().nullable().default(null), }); export const portabilitySidebarOrderSchema = z.object({ diff --git a/packages/shared/src/validators/company.ts b/packages/shared/src/validators/company.ts index e3a1a208..00507581 100644 --- a/packages/shared/src/validators/company.ts +++ b/packages/shared/src/validators/company.ts @@ -3,6 +3,7 @@ import { COMPANY_STATUSES } from "../constants.js"; const logoAssetIdSchema = z.string().uuid().nullable().optional(); const brandColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(); +const feedbackDataSharingTermsVersionSchema = z.string().min(1).nullable().optional(); export const createCompanySchema = z.object({ name: z.string().min(1), @@ -18,6 +19,10 @@ export const updateCompanySchema = createCompanySchema status: z.enum(COMPANY_STATUSES).optional(), spentMonthlyCents: z.number().int().nonnegative().optional(), requireBoardApprovalForNewAgents: z.boolean().optional(), + feedbackDataSharingEnabled: z.boolean().optional(), + feedbackDataSharingConsentAt: z.coerce.date().nullable().optional(), + feedbackDataSharingConsentByUserId: z.string().min(1).nullable().optional(), + feedbackDataSharingTermsVersion: feedbackDataSharingTermsVersionSchema, brandColor: brandColorSchema, logoAssetId: logoAssetIdSchema, }); diff --git a/packages/shared/src/validators/feedback.ts b/packages/shared/src/validators/feedback.ts new file mode 100644 index 00000000..9ac34ef4 --- /dev/null +++ b/packages/shared/src/validators/feedback.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; +import { + FEEDBACK_DATA_SHARING_PREFERENCES, + FEEDBACK_TARGET_TYPES, + FEEDBACK_TRACE_STATUSES, + FEEDBACK_VOTE_VALUES, +} from "../types/feedback.js"; + +export const feedbackTargetTypeSchema = z.enum(FEEDBACK_TARGET_TYPES); +export const feedbackTraceStatusSchema = z.enum(FEEDBACK_TRACE_STATUSES); +export const feedbackVoteValueSchema = z.enum(FEEDBACK_VOTE_VALUES); +export const feedbackDataSharingPreferenceSchema = z.enum(FEEDBACK_DATA_SHARING_PREFERENCES); + +export const upsertIssueFeedbackVoteSchema = z.object({ + targetType: feedbackTargetTypeSchema, + targetId: z.string().uuid(), + vote: feedbackVoteValueSchema, + reason: z.string().trim().max(1000).optional(), + allowSharing: z.boolean().optional(), +}); + +export type UpsertIssueFeedbackVote = z.infer; diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index 8d808ab0..696567b7 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -24,6 +24,14 @@ export { type UpdateCompany, type UpdateCompanyBranding, } from "./company.js"; +export { + feedbackDataSharingPreferenceSchema, + feedbackTargetTypeSchema, + feedbackTraceStatusSchema, + feedbackVoteValueSchema, + upsertIssueFeedbackVoteSchema, + type UpsertIssueFeedbackVote, +} from "./feedback.js"; export { companySkillSourceTypeSchema, companySkillTrustLevelSchema, diff --git a/packages/shared/src/validators/instance.ts b/packages/shared/src/validators/instance.ts index 05ee4323..4afad283 100644 --- a/packages/shared/src/validators/instance.ts +++ b/packages/shared/src/validators/instance.ts @@ -1,7 +1,12 @@ import { z } from "zod"; +import { DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE } from "../types/feedback.js"; +import { feedbackDataSharingPreferenceSchema } from "./feedback.js"; export const instanceGeneralSettingsSchema = z.object({ censorUsernameInLogs: z.boolean().default(false), + feedbackDataSharingPreference: feedbackDataSharingPreferenceSchema.default( + DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, + ), }).strict(); export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema.partial(); diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index e32894cb..801e0991 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -131,7 +131,7 @@ function makeAgent(adapterType: string) { describe("agent skill routes", () => { beforeEach(() => { - vi.clearAllMocks(); + vi.resetAllMocks(); mockAgentService.resolveByReference.mockResolvedValue({ ambiguous: false, agent: makeAgent("claude_local"), diff --git a/server/src/__tests__/companies-route-path-guard.test.ts b/server/src/__tests__/companies-route-path-guard.test.ts index aef2c292..414936a9 100644 --- a/server/src/__tests__/companies-route-path-guard.test.ts +++ b/server/src/__tests__/companies-route-path-guard.test.ts @@ -29,6 +29,12 @@ vi.mock("../services/index.js", () => ({ agentService: () => ({ getById: vi.fn(), }), + feedbackService: () => ({ + listIssueVotesForUser: vi.fn(), + listFeedbackTraces: vi.fn(), + getFeedbackTraceById: vi.fn(), + saveIssueVote: vi.fn(), + }), logActivity: vi.fn(), })); diff --git a/server/src/__tests__/company-branding-route.test.ts b/server/src/__tests__/company-branding-route.test.ts index 86d9441c..ddf16158 100644 --- a/server/src/__tests__/company-branding-route.test.ts +++ b/server/src/__tests__/company-branding-route.test.ts @@ -34,6 +34,12 @@ const mockCompanyPortabilityService = vi.hoisted(() => ({ })); const mockLogActivity = vi.hoisted(() => vi.fn()); +const mockFeedbackService = vi.hoisted(() => ({ + listIssueVotesForUser: vi.fn(), + listFeedbackTraces: vi.fn(), + getFeedbackTraceById: vi.fn(), + saveIssueVote: vi.fn(), +})); vi.mock("../services/index.js", () => ({ accessService: () => mockAccessService, @@ -41,6 +47,7 @@ vi.mock("../services/index.js", () => ({ budgetService: () => mockBudgetService, companyPortabilityService: () => mockCompanyPortabilityService, companyService: () => mockCompanyService, + feedbackService: () => mockFeedbackService, logActivity: mockLogActivity, })); @@ -78,9 +85,7 @@ function createApp(actor: Record) { describe("PATCH /api/companies/:companyId/branding", () => { beforeEach(() => { - mockCompanyService.update.mockReset(); - mockAgentService.getById.mockReset(); - mockLogActivity.mockReset(); + vi.resetAllMocks(); }); it("rejects non-CEO agent callers", async () => { diff --git a/server/src/__tests__/company-portability-routes.test.ts b/server/src/__tests__/company-portability-routes.test.ts index ab7c3d0d..d380050b 100644 --- a/server/src/__tests__/company-portability-routes.test.ts +++ b/server/src/__tests__/company-portability-routes.test.ts @@ -32,6 +32,12 @@ const mockCompanyPortabilityService = vi.hoisted(() => ({ })); const mockLogActivity = vi.hoisted(() => vi.fn()); +const mockFeedbackService = vi.hoisted(() => ({ + listIssueVotesForUser: vi.fn(), + listFeedbackTraces: vi.fn(), + getFeedbackTraceById: vi.fn(), + saveIssueVote: vi.fn(), +})); vi.mock("../services/index.js", () => ({ accessService: () => mockAccessService, @@ -39,6 +45,7 @@ vi.mock("../services/index.js", () => ({ budgetService: () => mockBudgetService, companyPortabilityService: () => mockCompanyPortabilityService, companyService: () => mockCompanyService, + feedbackService: () => mockFeedbackService, logActivity: mockLogActivity, })); diff --git a/server/src/__tests__/feedback-service.test.ts b/server/src/__tests__/feedback-service.test.ts new file mode 100644 index 00000000..af44f866 --- /dev/null +++ b/server/src/__tests__/feedback-service.test.ts @@ -0,0 +1,1103 @@ +import { randomUUID } from "node:crypto"; +import fs from "node:fs"; +import net from "node:net"; +import os from "node:os"; +import path from "node:path"; +import { eq } from "drizzle-orm"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { writePaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; +import { + agents, + applyPendingMigrations, + companies, + companySkills, + costEvents, + createDb, + documents, + documentRevisions, + ensurePostgresDatabase, + feedbackExports, + feedbackVotes, + heartbeatRuns, + instanceSettings, + issueComments, + issueDocuments, + issues, +} from "@paperclipai/db"; +import { feedbackService } from "../services/feedback.ts"; + +type EmbeddedPostgresInstance = { + initialise(): Promise; + start(): Promise; + stop(): Promise; +}; + +type EmbeddedPostgresCtor = new (opts: { + databaseDir: string; + user: string; + password: string; + port: number; + persistent: boolean; + initdbFlags?: string[]; + onLog?: (message: unknown) => void; + onError?: (message: unknown) => void; +}) => EmbeddedPostgresInstance; + +async function getEmbeddedPostgresCtor(): Promise { + const mod = await import("embedded-postgres"); + return mod.default as EmbeddedPostgresCtor; +} + +async function getAvailablePort(): Promise { + return await new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(() => reject(new Error("Failed to allocate test port"))); + return; + } + const { port } = address; + server.close((error) => { + if (error) reject(error); + else resolve(port); + }); + }); + }); +} + +async function startTempDatabase() { + const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-feedback-service-")); + const port = await getAvailablePort(); + const EmbeddedPostgres = await getEmbeddedPostgresCtor(); + const instance = new EmbeddedPostgres({ + databaseDir: dataDir, + user: "paperclip", + password: "paperclip", + port, + persistent: true, + initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"], + onLog: () => {}, + onError: () => {}, + }); + await instance.initialise(); + await instance.start(); + + const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`; + await ensurePostgresDatabase(adminConnectionString, "paperclip"); + const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`; + await applyPendingMigrations(connectionString); + return { connectionString, dataDir, instance }; +} + +describe("feedbackService.saveIssueVote", () => { + let db!: ReturnType; + let svc!: ReturnType; + let instance: EmbeddedPostgresInstance | null = null; + let dataDir = ""; + let tempDirs: string[] = []; + + beforeAll(async () => { + const started = await startTempDatabase(); + db = createDb(started.connectionString); + svc = feedbackService(db); + instance = started.instance; + dataDir = started.dataDir; + }, 20_000); + + afterEach(async () => { + await db.delete(feedbackExports); + await db.delete(feedbackVotes); + await db.delete(instanceSettings); + await db.delete(issueDocuments); + await db.delete(documentRevisions); + await db.delete(documents); + await db.delete(issueComments); + await db.delete(costEvents); + await db.delete(heartbeatRuns); + await db.delete(companySkills); + await db.delete(issues); + await db.delete(agents); + await db.delete(companies); + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } + vi.unstubAllEnvs(); + tempDirs = []; + }); + + afterAll(async () => { + await instance?.stop(); + if (dataDir) { + fs.rmSync(dataDir, { recursive: true, force: true }); + } + }); + + async function seedIssueWithAgentComment() { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const commentId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `F${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "active", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Add feedback voting", + status: "todo", + priority: "medium", + createdByUserId: "user-1", + }); + + await db.insert(issueComments).values({ + id: commentId, + companyId, + issueId, + authorAgentId: agentId, + body: "AI generated update", + }); + + return { companyId, issueId, commentId }; + } + + async function seedIssueWithRichAgentComment() { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const targetCommentId = randomUUID(); + const earlierCommentId = randomUUID(); + const laterCommentId = randomUUID(); + const runId = randomUUID(); + const instructionsDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-feedback-instructions-")); + tempDirs.push(instructionsDir); + const instructionsPath = path.join(instructionsDir, "AGENTS.md"); + fs.writeFileSync( + instructionsPath, + "You are a coder.\nUse api_key=secret-value.\nPrefer /Users/dotta/private-workspace.", + "utf8", + ); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `R${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(companySkills).values([ + { + id: randomUUID(), + companyId, + key: "paperclipai/paperclip/paperclip", + slug: "paperclip", + name: "Paperclip", + markdown: "# Paperclip", + sourceType: "catalog", + sourceLocator: null, + sourceRef: null, + fileInventory: [{ path: "SKILL.md", kind: "skill" }], + }, + { + id: randomUUID(), + companyId, + key: "octo/research/public-skill", + slug: "public-skill", + name: "Public Skill", + markdown: "# Public Skill", + sourceType: "github", + sourceLocator: "https://github.com/octo/research/tree/main/skills/public-skill", + sourceRef: "main", + fileInventory: [{ path: "SKILL.md", kind: "skill" }], + }, + ]); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "active", + adapterType: "codex_local", + adapterConfig: writePaperclipSkillSyncPreference( + { + model: "gpt-5.4", + instructionsBundleMode: "external", + instructionsRootPath: instructionsDir, + instructionsEntryFile: "AGENTS.md", + instructionsFilePath: instructionsPath, + }, + ["paperclipai/paperclip/paperclip", "octo/research/public-skill"], + ), + runtimeConfig: { + heartbeat: { + enabled: true, + intervalSec: 3600, + }, + }, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Trace-rich feedback", + description: "Issue context includes ops@example.com and a backup phone 555 111 2222.", + status: "todo", + priority: "medium", + createdByUserId: "user-1", + }); + + await db.insert(heartbeatRuns).values({ + id: runId, + companyId, + agentId, + invocationSource: "manual", + status: "succeeded", + startedAt: new Date("2026-03-30T10:00:00.000Z"), + finishedAt: new Date("2026-03-30T10:05:00.000Z"), + usageJson: { + provider: "openai", + model: "gpt-5.4", + inputTokens: 123, + outputTokens: 45, + costUsd: 0.12, + }, + }); + + await db.insert(costEvents).values({ + id: randomUUID(), + companyId, + agentId, + issueId, + heartbeatRunId: runId, + provider: "openai", + biller: "openai", + billingType: "metered", + model: "gpt-5.4", + inputTokens: 123, + cachedInputTokens: 0, + outputTokens: 45, + costCents: 12, + occurredAt: new Date("2026-03-30T10:05:00.000Z"), + }); + + await db.insert(issueComments).values([ + { + id: earlierCommentId, + companyId, + issueId, + authorAgentId: agentId, + createdByRunId: runId, + body: "Previous comment with ops@example.com in it.", + createdAt: new Date("2026-03-30T10:01:00.000Z"), + }, + { + id: targetCommentId, + companyId, + issueId, + authorAgentId: agentId, + createdByRunId: runId, + body: "Target output with api_key=secret-value and Bearer secret-token.", + createdAt: new Date("2026-03-30T10:02:00.000Z"), + }, + { + id: laterCommentId, + companyId, + issueId, + authorAgentId: agentId, + createdByRunId: runId, + body: "Later comment mentions 555 111 2222 for follow-up.", + createdAt: new Date("2026-03-30T10:03:00.000Z"), + }, + ]); + + return { companyId, issueId, targetCommentId, runId }; + } + + async function seedIssueWithAgentDocument() { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const documentId = randomUUID(); + const revisionId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `D${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "active", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Document feedback", + status: "todo", + priority: "medium", + createdByUserId: "user-1", + }); + + await db.insert(documents).values({ + id: documentId, + companyId, + title: "Plan", + format: "markdown", + latestBody: "Drafted by an agent", + latestRevisionId: revisionId, + latestRevisionNumber: 1, + createdByAgentId: agentId, + updatedByAgentId: agentId, + }); + + await db.insert(documentRevisions).values({ + id: revisionId, + companyId, + documentId, + revisionNumber: 1, + body: "Drafted by an agent", + createdByAgentId: agentId, + }); + + await db.insert(issueDocuments).values({ + companyId, + issueId, + documentId, + key: "plan", + }); + + return { companyId, issueId, revisionId }; + } + + async function seedIssueWithAdapterRunComment(input: { + adapterType: "claude_local" | "opencode_local"; + sessionId: string; + }) { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const commentId = randomUUID(); + const runId = randomUUID(); + + 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: "TraceCollector", + role: "engineer", + status: "active", + adapterType: input.adapterType, + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Trace-backed feedback", + status: "todo", + priority: "medium", + createdByUserId: "user-1", + }); + + await db.insert(heartbeatRuns).values({ + id: runId, + companyId, + agentId, + invocationSource: "manual", + status: "succeeded", + sessionIdAfter: input.sessionId, + startedAt: new Date("2026-04-01T10:00:00.000Z"), + finishedAt: new Date("2026-04-01T10:05:00.000Z"), + usageJson: { + provider: input.adapterType === "claude_local" ? "anthropic" : "opencode", + model: input.adapterType === "claude_local" ? "claude-opus-4-6" : "opencode/minimax-m2.5-free", + }, + }); + + await db.insert(issueComments).values({ + id: commentId, + companyId, + issueId, + authorAgentId: agentId, + createdByRunId: runId, + body: "Trace-backed agent output", + }); + + return { companyId, issueId, commentId }; + } + + it("stores a local vote without enabling sharing by default", async () => { + const { companyId, issueId, commentId } = await seedIssueWithAgentComment(); + + const result = await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "up", + authorUserId: "user-1", + }); + + expect(result.vote.vote).toBe("up"); + expect(result.sharingEnabled).toBe(false); + expect(result.persistedSharingPreference).toBe("not_allowed"); + expect(result.vote.consentVersion).toBeNull(); + + const company = await db + .select() + .from(companies) + .where(eq(companies.id, companyId)) + .then((rows) => rows[0] ?? null); + + expect(company?.feedbackDataSharingEnabled).toBe(false); + expect(company?.feedbackDataSharingConsentAt).toBeNull(); + + const settings = await db + .select() + .from(instanceSettings) + .where(eq(instanceSettings.singletonKey, "default")) + .then((rows) => rows[0] ?? null); + + expect(settings?.general).toMatchObject({ + feedbackDataSharingPreference: "not_allowed", + }); + + const traces = await svc.listFeedbackTraces({ + companyId, + issueId, + includePayload: true, + }); + expect(traces[0]?.payloadSnapshot?.bundle).toBeNull(); + expect(traces[0]?.exportId).toBeNull(); + }); + + it("enables sharing metadata on the first consented vote and upserts subsequent votes", async () => { + const { companyId, issueId, commentId } = await seedIssueWithAgentComment(); + + const first = await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "up", + authorUserId: "user-1", + allowSharing: true, + }); + + expect(first.consentEnabledNow).toBe(true); + expect(first.sharingEnabled).toBe(true); + expect(first.persistedSharingPreference).toBe("allowed"); + expect(first.vote.sharedWithLabs).toBe(true); + expect(first.vote.sharedAt).toBeInstanceOf(Date); + expect(first.vote.consentVersion).toBe("feedback-data-sharing-v1"); + + const second = await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "down", + authorUserId: "user-1", + }); + + expect(second.consentEnabledNow).toBe(false); + expect(second.sharingEnabled).toBe(false); + expect(second.persistedSharingPreference).toBeNull(); + expect(second.vote.vote).toBe("down"); + expect(second.vote.sharedWithLabs).toBe(false); + expect(second.vote.sharedAt).toBeNull(); + expect(second.vote.consentVersion).toBeNull(); + + const votes = await svc.listIssueVotesForUser(issueId, "user-1"); + expect(votes).toHaveLength(1); + expect(votes[0]?.vote).toBe("down"); + expect(votes[0]?.sharedWithLabs).toBe(false); + expect(votes[0]?.consentVersion).toBeNull(); + + const company = await db + .select() + .from(companies) + .where(eq(companies.id, companyId)) + .then((rows) => rows[0] ?? null); + + expect(company?.feedbackDataSharingEnabled).toBe(true); + expect(company?.feedbackDataSharingConsentByUserId).toBe("user-1"); + expect(company?.feedbackDataSharingTermsVersion).toBe("feedback-data-sharing-v1"); + + const settings = await db + .select() + .from(instanceSettings) + .where(eq(instanceSettings.singletonKey, "default")) + .then((rows) => rows[0] ?? null); + + expect(settings?.general).toMatchObject({ + feedbackDataSharingPreference: "allowed", + }); + }); + + it("stores a trace record for document revision feedback targets", async () => { + const { issueId, revisionId } = await seedIssueWithAgentDocument(); + + const result = await svc.saveIssueVote({ + issueId, + targetType: "issue_document_revision", + targetId: revisionId, + vote: "up", + authorUserId: "user-1", + allowSharing: true, + }); + + expect(result.vote.vote).toBe("up"); + expect(result.sharingEnabled).toBe(true); + + const traces = await svc.listFeedbackTraces({ + companyId: result.vote.companyId, + issueId, + includePayload: true, + }); + + expect(traces).toHaveLength(1); + expect(traces[0]?.targetType).toBe("issue_document_revision"); + expect(traces[0]?.status).toBe("pending"); + expect(traces[0]?.targetSummary.documentKey).toBe("plan"); + expect(traces[0]?.targetSummary.revisionNumber).toBe(1); + expect(traces[0]?.payloadSnapshot?.target).toMatchObject({ + type: "issue_document_revision", + id: revisionId, + documentKey: "plan", + revisionNumber: 1, + }); + }); + + it("stores a downvote reason and includes it in the trace payload", async () => { + const { issueId, commentId } = await seedIssueWithAgentComment(); + + const result = await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "down", + reason: "The update missed the edge case handling.", + authorUserId: "user-1", + }); + + expect(result.vote.reason).toBe("The update missed the edge case handling."); + + const traces = await svc.listFeedbackTraces({ + companyId: result.vote.companyId, + issueId, + includePayload: true, + }); + + expect(traces[0]?.payloadSnapshot?.vote).toMatchObject({ + value: "down", + reason: "The update missed the edge case handling.", + sharedWithLabs: false, + }); + }); + + it("updates an existing downvote reason in place without creating a second trace", async () => { + const { issueId, commentId } = await seedIssueWithAgentComment(); + + const firstResult = await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "down", + authorUserId: "user-1", + }); + + const secondResult = await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "down", + reason: "Needed concrete next steps.", + authorUserId: "user-1", + }); + + expect(secondResult.vote.id).toBe(firstResult.vote.id); + expect(secondResult.vote.reason).toBe("Needed concrete next steps."); + + const traces = await svc.listFeedbackTraces({ + companyId: secondResult.vote.companyId, + issueId, + includePayload: true, + }); + + expect(traces).toHaveLength(1); + expect(traces[0]?.feedbackVoteId).toBe(firstResult.vote.id); + expect(traces[0]?.payloadSnapshot?.vote).toMatchObject({ + value: "down", + reason: "Needed concrete next steps.", + sharedWithLabs: false, + }); + }); + + it("builds a detailed sanitized shared bundle with issue and agent context", async () => { + const { companyId, issueId, targetCommentId, runId } = await seedIssueWithRichAgentComment(); + + await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: targetCommentId, + vote: "up", + authorUserId: "user-1", + allowSharing: true, + }); + + const traces = await svc.listFeedbackTraces({ + companyId, + issueId, + includePayload: true, + }); + const trace = traces[0]; + const payload = trace?.payloadSnapshot; + const bundle = payload?.bundle as Record | null; + const primaryContent = bundle?.primaryContent as Record | null; + const issueContext = bundle?.issueContext as Record | null; + const issueContextItems = issueContext?.items as Array> | undefined; + const agentContext = bundle?.agentContext as Record | null; + const runtime = agentContext?.runtime as Record | null; + const sourceRun = runtime?.sourceRun as Record | null; + const skills = agentContext?.skills as Record | null; + const skillItems = skills?.items as Array> | undefined; + const instructions = agentContext?.instructions as Record | null; + + expect(trace?.status).toBe("pending"); + expect(trace?.exportId).toMatch(/^fbexp_/); + expect(trace?.schemaVersion).toBe("paperclip-feedback-envelope-v2"); + expect(trace?.bundleVersion).toBe("paperclip-feedback-bundle-v2"); + expect(trace?.payloadDigest).toMatch(/^[a-f0-9]{64}$/); + expect(primaryContent?.createdByRunId).toBe(runId); + expect(String(primaryContent?.body)).toContain("[REDACTED]"); + expect(String(primaryContent?.body)).not.toContain("secret-value"); + expect(issueContextItems).toHaveLength(2); + expect(JSON.stringify(issueContextItems)).toContain("[REDACTED_EMAIL]"); + expect(JSON.stringify(issueContextItems)).toContain("[REDACTED_PHONE]"); + expect(sourceRun?.id).toBe(runId); + expect(JSON.stringify(sourceRun)).toContain("gpt-5.4"); + expect(skillItems?.[1]?.sourceLocator).toBe("https://github.com/octo/research/tree/main/skills/public-skill"); + expect(String(instructions?.entryBody)).toContain("[REDACTED]"); + expect(String(instructions?.entryBody)).not.toContain("secret-value"); + }); + + it("keeps earlier local votes local when a later vote enables sharing", async () => { + const { companyId, issueId, commentId: firstCommentId } = await seedIssueWithAgentComment(); + const secondCommentId = randomUUID(); + const agentId = await db + .select({ authorAgentId: issueComments.authorAgentId }) + .from(issueComments) + .where(eq(issueComments.id, firstCommentId)) + .then((rows) => rows[0]?.authorAgentId ?? null); + + await db.insert(issueComments).values({ + id: secondCommentId, + companyId, + issueId, + authorAgentId: agentId, + body: "Second AI generated update", + }); + + await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: firstCommentId, + vote: "up", + authorUserId: "user-1", + }); + + await svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: secondCommentId, + vote: "up", + authorUserId: "user-1", + allowSharing: true, + }); + + const traces = await svc.listFeedbackTraces({ + companyId, + issueId, + includePayload: true, + }); + const localTrace = traces.find((trace) => trace.targetId === firstCommentId); + const sharedTrace = traces.find((trace) => trace.targetId === secondCommentId); + + expect(localTrace?.status).toBe("local_only"); + expect(localTrace?.exportId).toBeNull(); + expect(localTrace?.payloadSnapshot?.bundle).toBeNull(); + expect(sharedTrace?.status).toBe("pending"); + expect(sharedTrace?.exportId).toMatch(/^fbexp_/); + }); + + it("captures Claude project session artifacts as full traces", async () => { + const claudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-feedback-claude-")); + tempDirs.push(claudeRoot); + const sessionId = randomUUID(); + const projectDir = path.join(claudeRoot, "projects", "workspace-1"); + fs.mkdirSync(path.join(projectDir, sessionId, "tool-results"), { recursive: true }); + fs.mkdirSync(path.join(claudeRoot, "debug"), { recursive: true }); + fs.writeFileSync( + path.join(projectDir, `${sessionId}.jsonl`), + [ + JSON.stringify({ + type: "user", + sessionId, + message: { role: "user", content: "Open AGENTS.md and continue the task." }, + }), + JSON.stringify({ + type: "assistant", + sessionId, + message: { + role: "assistant", + content: [{ type: "tool_use", name: "Read", input: { file_path: "/tmp/AGENTS.md" } }], + }, + }), + ].join("\n"), + "utf8", + ); + fs.writeFileSync( + path.join(projectDir, sessionId, "tool-results", "result.txt"), + "Read tool output with api_key=secret-value", + "utf8", + ); + fs.writeFileSync( + path.join(claudeRoot, "debug", `${sessionId}.txt`), + "Claude debug log with /Users/dotta/private-workspace and api_key=secret-value", + "utf8", + ); + vi.stubEnv("CLAUDE_CONFIG_DIR", claudeRoot); + const uploadTraceBundle = vi.fn().mockResolvedValue({ objectKey: "feedback-traces/test.json" }); + const flushingSvc = feedbackService(db, { + shareClient: { + uploadTraceBundle, + }, + }); + + const { issueId, commentId } = await seedIssueWithAdapterRunComment({ + adapterType: "claude_local", + sessionId, + }); + + await flushingSvc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "up", + authorUserId: "user-1", + allowSharing: true, + }); + await flushingSvc.flushPendingFeedbackTraces(); + + expect(uploadTraceBundle).toHaveBeenCalledTimes(1); + const bundle = uploadTraceBundle.mock.calls[0]?.[0] as Record | undefined; + const files = Array.isArray(bundle?.files) ? (bundle.files as Array>) : []; + const filePaths = files.map((file) => String(file.path)); + const rawAdapterTrace = bundle?.rawAdapterTrace as Record | null; + + expect(bundle?.captureStatus).toBe("full"); + expect(filePaths).toContain("adapter/claude/session.jsonl"); + expect(filePaths).toContain("adapter/claude/session/tool-results/result.txt"); + expect(filePaths).toContain("adapter/claude/debug.txt"); + expect(rawAdapterTrace?.projectSessionFound).toBe(true); + expect(rawAdapterTrace?.projectArtifactsCount).toBe(1); + expect(rawAdapterTrace?.debugLogFound).toBe(true); + }); + + it("captures OpenCode message and part files as full traces", async () => { + const opencodeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-feedback-opencode-")); + tempDirs.push(opencodeRoot); + const sessionId = "ses_test_feedback_trace"; + const sessionDir = path.join(opencodeRoot, "storage", "session", "global"); + const messageDir = path.join(opencodeRoot, "storage", "message", sessionId); + const partDir = path.join(opencodeRoot, "storage", "part"); + fs.mkdirSync(sessionDir, { recursive: true }); + fs.mkdirSync(path.join(opencodeRoot, "storage", "session_diff"), { recursive: true }); + fs.mkdirSync(messageDir, { recursive: true }); + fs.mkdirSync(path.join(opencodeRoot, "storage", "project"), { recursive: true }); + fs.mkdirSync(path.join(opencodeRoot, "storage", "todo"), { recursive: true }); + const userMessageId = "msg_user_trace"; + const assistantMessageId = "msg_assistant_trace"; + fs.mkdirSync(path.join(partDir, userMessageId), { recursive: true }); + fs.mkdirSync(path.join(partDir, assistantMessageId), { recursive: true }); + + fs.writeFileSync( + path.join(sessionDir, `${sessionId}.json`), + JSON.stringify({ + id: sessionId, + projectID: "project-trace", + title: "Feedback export verification", + }), + "utf8", + ); + fs.writeFileSync( + path.join(opencodeRoot, "storage", "session_diff", `${sessionId}.json`), + JSON.stringify([{ op: "replace", path: "/title", value: "Feedback export verification" }]), + "utf8", + ); + fs.writeFileSync( + path.join(messageDir, `${userMessageId}.json`), + JSON.stringify({ + id: userMessageId, + sessionID: sessionId, + role: "user", + summary: { title: "Continue the issue" }, + }), + "utf8", + ); + fs.writeFileSync( + path.join(messageDir, `${assistantMessageId}.json`), + JSON.stringify({ + id: assistantMessageId, + sessionID: sessionId, + role: "assistant", + finish: "tool-calls", + }), + "utf8", + ); + fs.writeFileSync( + path.join(partDir, userMessageId, "prt_prompt.json"), + JSON.stringify({ + id: "prt_prompt", + sessionID: sessionId, + messageID: userMessageId, + type: "text", + text: "Open AGENTS.md and continue the task.", + }), + "utf8", + ); + fs.writeFileSync( + path.join(partDir, assistantMessageId, "prt_tool.json"), + JSON.stringify({ + id: "prt_tool", + sessionID: sessionId, + messageID: assistantMessageId, + type: "tool", + tool: "read", + state: { + status: "completed", + input: { filePath: "/tmp/AGENTS.md" }, + output: "api_key=secret-value", + }, + }), + "utf8", + ); + fs.writeFileSync( + path.join(opencodeRoot, "storage", "project", "project-trace.json"), + JSON.stringify({ + id: "project-trace", + worktree: "/Users/dotta/project", + }), + "utf8", + ); + fs.writeFileSync( + path.join(opencodeRoot, "storage", "todo", `${sessionId}.json`), + JSON.stringify([{ content: "Verify exported traces" }]), + "utf8", + ); + vi.stubEnv("PAPERCLIP_OPENCODE_STORAGE_DIR", opencodeRoot); + const uploadTraceBundle = vi.fn().mockResolvedValue({ objectKey: "feedback-traces/test.json" }); + const flushingSvc = feedbackService(db, { + shareClient: { + uploadTraceBundle, + }, + }); + + const { issueId, commentId } = await seedIssueWithAdapterRunComment({ + adapterType: "opencode_local", + sessionId, + }); + + await flushingSvc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "up", + authorUserId: "user-1", + allowSharing: true, + }); + await flushingSvc.flushPendingFeedbackTraces(); + + expect(uploadTraceBundle).toHaveBeenCalledTimes(1); + const bundle = uploadTraceBundle.mock.calls[0]?.[0] as Record | undefined; + const files = Array.isArray(bundle?.files) ? (bundle.files as Array>) : []; + const filePaths = files.map((file) => String(file.path)); + const rawAdapterTrace = bundle?.rawAdapterTrace as Record | null; + + expect(bundle?.captureStatus).toBe("full"); + expect(filePaths).toContain("adapter/opencode/session.json"); + expect(filePaths).toContain("adapter/opencode/session-diff.json"); + expect(filePaths).toContain(`adapter/opencode/messages/${userMessageId}.json`); + expect(filePaths).toContain(`adapter/opencode/parts/${assistantMessageId}/prt_tool.json`); + expect(filePaths).toContain("adapter/opencode/project.json"); + expect(filePaths).toContain("adapter/opencode/todo.json"); + expect(rawAdapterTrace?.messageFilesCount).toBe(2); + expect(rawAdapterTrace?.partFilesCount).toBe(2); + }); + + it("rejects feedback votes on human-authored comments", async () => { + const companyId = randomUUID(); + const issueId = randomUUID(); + const commentId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `H${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Human-authored comment", + status: "todo", + priority: "medium", + createdByUserId: "user-1", + }); + + await db.insert(issueComments).values({ + id: commentId, + companyId, + issueId, + authorUserId: "user-2", + body: "Board comment", + }); + + await expect( + svc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "up", + authorUserId: "user-1", + }), + ).rejects.toThrow("Feedback voting is only available on agent-authored issue comments"); + }); + + it("flushes pending shared traces into configured object storage and marks them sent", async () => { + const { companyId, issueId, commentId } = await seedIssueWithAgentComment(); + const uploadTraceBundle = vi.fn().mockResolvedValue({ + objectKey: `feedback-traces/${companyId}/2026/04/01/test-trace.json`, + }); + const flushingSvc = feedbackService(db, { + shareClient: { + uploadTraceBundle, + }, + }); + + await flushingSvc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "up", + authorUserId: "user-1", + allowSharing: true, + }); + + const flushResult = await flushingSvc.flushPendingFeedbackTraces(); + expect(flushResult).toMatchObject({ + attempted: 1, + sent: 1, + failed: 0, + }); + + const traces = await flushingSvc.listFeedbackTraces({ + companyId, + issueId, + includePayload: true, + }); + expect(traces[0]?.status).toBe("sent"); + expect(traces[0]?.attemptCount).toBe(1); + expect(traces[0]?.exportedAt).toBeInstanceOf(Date); + expect(traces[0]?.failureReason).toBeNull(); + expect(uploadTraceBundle).toHaveBeenCalledTimes(1); + expect(uploadTraceBundle.mock.calls[0]?.[0]).toMatchObject({ + traceId: traces[0]?.id, + exportId: traces[0]?.exportId, + companyId, + issueId, + issueIdentifier: traces[0]?.issueIdentifier, + captureStatus: expect.stringMatching(/^(full|partial|unavailable)$/), + envelope: { + destination: "paperclip_labs_feedback_v1", + exportId: traces[0]?.exportId, + }, + }); + }); + + it("marks pending shared traces as failed when remote export upload fails", async () => { + const { companyId, issueId, commentId } = await seedIssueWithAgentComment(); + const uploadTraceBundle = vi.fn().mockRejectedValue(new Error("telemetry unavailable")); + const flushingSvc = feedbackService(db, { + shareClient: { + uploadTraceBundle, + }, + }); + + await flushingSvc.saveIssueVote({ + issueId, + targetType: "issue_comment", + targetId: commentId, + vote: "up", + authorUserId: "user-1", + allowSharing: true, + }); + + const flushResult = await flushingSvc.flushPendingFeedbackTraces(); + expect(flushResult).toMatchObject({ + attempted: 1, + sent: 0, + failed: 1, + }); + + const traces = await flushingSvc.listFeedbackTraces({ + companyId, + issueId, + includePayload: true, + }); + expect(traces[0]?.status).toBe("failed"); + expect(traces[0]?.attemptCount).toBe(1); + expect(traces[0]?.lastAttemptedAt).toBeInstanceOf(Date); + expect(traces[0]?.failureReason).toContain("telemetry unavailable"); + expect(traces[0]?.exportedAt).toBeNull(); + expect(uploadTraceBundle).toHaveBeenCalledTimes(1); + }); +}); diff --git a/server/src/__tests__/instance-settings-routes.test.ts b/server/src/__tests__/instance-settings-routes.test.ts index 9668d1bf..4d8e12b1 100644 --- a/server/src/__tests__/instance-settings-routes.test.ts +++ b/server/src/__tests__/instance-settings-routes.test.ts @@ -35,6 +35,7 @@ describe("instance settings routes", () => { vi.clearAllMocks(); mockInstanceSettingsService.getGeneral.mockResolvedValue({ censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", }); mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false, @@ -44,6 +45,7 @@ describe("instance settings routes", () => { id: "instance-settings-1", general: { censorUsernameInLogs: true, + feedbackDataSharingPreference: "allowed", }, }); mockInstanceSettingsService.updateExperimental.mockResolvedValue({ @@ -110,15 +112,22 @@ describe("instance settings routes", () => { const getRes = await request(app).get("/api/instance/settings/general"); expect(getRes.status).toBe(200); - expect(getRes.body).toEqual({ censorUsernameInLogs: false }); + expect(getRes.body).toEqual({ + censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", + }); const patchRes = await request(app) .patch("/api/instance/settings/general") - .send({ censorUsernameInLogs: true }); + .send({ + censorUsernameInLogs: true, + feedbackDataSharingPreference: "allowed", + }); expect(patchRes.status).toBe(200); expect(mockInstanceSettingsService.updateGeneral).toHaveBeenCalledWith({ censorUsernameInLogs: true, + feedbackDataSharingPreference: "allowed", }); expect(mockLogActivity).toHaveBeenCalledTimes(2); }); @@ -148,7 +157,7 @@ describe("instance settings routes", () => { const res = await request(app) .patch("/api/instance/settings/general") - .send({ censorUsernameInLogs: true }); + .send({ feedbackDataSharingPreference: "not_allowed" }); expect(res.status).toBe(403); expect(mockInstanceSettingsService.updateGeneral).not.toHaveBeenCalled(); diff --git a/server/src/__tests__/issue-comment-reopen-routes.test.ts b/server/src/__tests__/issue-comment-reopen-routes.test.ts index 21bb44aa..4b000bfc 100644 --- a/server/src/__tests__/issue-comment-reopen-routes.test.ts +++ b/server/src/__tests__/issue-comment-reopen-routes.test.ts @@ -35,8 +35,22 @@ vi.mock("../services/index.js", () => ({ agentService: () => mockAgentService, documentService: () => ({}), executionWorkspaceService: () => ({}), + feedbackService: () => ({ + listIssueVotesForUser: vi.fn(async () => []), + saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })), + }), goalService: () => ({}), heartbeatService: () => mockHeartbeatService, + instanceSettingsService: () => ({ + get: vi.fn(async () => ({ + id: "instance-settings-1", + general: { + censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", + }, + })), + listCompanyIds: vi.fn(async () => ["company-1"]), + }), issueApprovalService: () => ({}), issueService: () => mockIssueService, logActivity: mockLogActivity, diff --git a/server/src/__tests__/issue-document-restore-routes.test.ts b/server/src/__tests__/issue-document-restore-routes.test.ts index b8ce2a9e..b8a57168 100644 --- a/server/src/__tests__/issue-document-restore-routes.test.ts +++ b/server/src/__tests__/issue-document-restore-routes.test.ts @@ -32,11 +32,16 @@ vi.mock("../services/index.js", () => ({ agentService: () => mockAgentService, documentService: () => mockDocumentsService, executionWorkspaceService: () => ({}), + feedbackService: () => ({}), goalService: () => ({}), heartbeatService: () => ({ wakeup: vi.fn(async () => undefined), reportRunActivity: vi.fn(async () => undefined), }), + instanceSettingsService: () => ({ + getExperimental: vi.fn(async () => ({})), + getGeneral: vi.fn(async () => ({ feedbackDataSharingPreference: "prompt" })), + }), issueApprovalService: () => ({}), issueService: () => mockIssueService, logActivity: mockLogActivity, diff --git a/server/src/__tests__/issues-goal-context-routes.test.ts b/server/src/__tests__/issues-goal-context-routes.test.ts index b4611d39..25ce2042 100644 --- a/server/src/__tests__/issues-goal-context-routes.test.ts +++ b/server/src/__tests__/issues-goal-context-routes.test.ts @@ -36,11 +36,25 @@ vi.mock("../services/index.js", () => ({ executionWorkspaceService: () => ({ getById: vi.fn(), }), + feedbackService: () => ({ + listIssueVotesForUser: vi.fn(async () => []), + saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })), + }), goalService: () => mockGoalService, heartbeatService: () => ({ wakeup: vi.fn(async () => undefined), reportRunActivity: vi.fn(async () => undefined), }), + instanceSettingsService: () => ({ + get: vi.fn(async () => ({ + id: "instance-settings-1", + general: { + censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", + }, + })), + listCompanyIds: vi.fn(async () => ["company-1"]), + }), issueApprovalService: () => ({}), issueService: () => mockIssueService, logActivity: vi.fn(async () => undefined), diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 9543a614..f6bcddd5 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -228,6 +228,42 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { expect(result.map((issue) => issue.id)).toEqual([matchedIssueId]); }); + it("accepts issue identifiers through getById", async () => { + const companyId = randomUUID(); + const issueId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: "PAP", + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + issueNumber: 1064, + identifier: "PAP-1064", + title: "Feedback votes error", + status: "todo", + priority: "medium", + createdByUserId: "user-1", + }); + + const issue = await svc.getById("PAP-1064"); + + expect(issue).toEqual( + expect.objectContaining({ + id: issueId, + identifier: "PAP-1064", + }), + ); + }); + + it("returns null instead of throwing for malformed non-uuid issue refs", async () => { + await expect(svc.getById("not-a-uuid")).resolves.toBeNull(); + }); + it("filters issues by execution workspace id", async () => { const companyId = randomUUID(); const projectId = randomUUID(); @@ -357,18 +393,8 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { }, ]); - await svc.archiveInbox( - companyId, - archivedIssueId, - userId, - new Date("2026-03-26T12:30:00.000Z"), - ); - await svc.archiveInbox( - companyId, - resurfacedIssueId, - userId, - new Date("2026-03-26T13:00:00.000Z"), - ); + await svc.archiveInbox(companyId, archivedIssueId, userId, new Date("2026-03-26T12:30:00.000Z")); + await svc.archiveInbox(companyId, resurfacedIssueId, userId, new Date("2026-03-26T13:00:00.000Z")); await db.insert(issueComments).values({ companyId, diff --git a/server/src/__tests__/server-startup-feedback-export.test.ts b/server/src/__tests__/server-startup-feedback-export.test.ts new file mode 100644 index 00000000..78565645 --- /dev/null +++ b/server/src/__tests__/server-startup-feedback-export.test.ts @@ -0,0 +1,173 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { + createAppMock, + createDbMock, + detectPortMock, + feedbackExportServiceMock, + feedbackServiceFactoryMock, + fakeServer, +} = vi.hoisted(() => { + const createAppMock = vi.fn(async () => ((_: unknown, __: unknown) => {}) as never); + const createDbMock = vi.fn(() => ({}) as never); + const detectPortMock = vi.fn(async (port: number) => port); + const feedbackExportServiceMock = { + flushPendingFeedbackTraces: vi.fn(async () => ({ attempted: 0, sent: 0, failed: 0 })), + }; + const feedbackServiceFactoryMock = vi.fn(() => feedbackExportServiceMock); + const fakeServer = { + once: vi.fn().mockReturnThis(), + off: vi.fn().mockReturnThis(), + listen: vi.fn((_port: number, _host: string, callback?: () => void) => { + callback?.(); + return fakeServer; + }), + close: vi.fn(), + }; + + return { + createAppMock, + createDbMock, + detectPortMock, + feedbackExportServiceMock, + feedbackServiceFactoryMock, + fakeServer, + }; +}); + +vi.mock("node:http", () => ({ + createServer: vi.fn(() => fakeServer), +})); + +vi.mock("detect-port", () => ({ + default: detectPortMock, +})); + +vi.mock("@paperclipai/db", () => ({ + createDb: createDbMock, + ensurePostgresDatabase: vi.fn(), + getPostgresDataDirectory: vi.fn(), + inspectMigrations: vi.fn(async () => ({ status: "upToDate" })), + applyPendingMigrations: vi.fn(), + reconcilePendingMigrationHistory: vi.fn(async () => ({ repairedMigrations: [] })), + formatDatabaseBackupResult: vi.fn(() => "ok"), + runDatabaseBackup: vi.fn(), + authUsers: {}, + companies: {}, + companyMemberships: {}, + instanceUserRoles: {}, +})); + +vi.mock("../app.js", () => ({ + createApp: createAppMock, +})); + +vi.mock("../config.js", () => ({ + loadConfig: vi.fn(() => ({ + deploymentMode: "authenticated", + deploymentExposure: "private", + host: "127.0.0.1", + port: 3210, + allowedHostnames: [], + authBaseUrlMode: "auto", + authPublicBaseUrl: undefined, + authDisableSignUp: false, + databaseMode: "postgres", + databaseUrl: "postgres://paperclip:paperclip@127.0.0.1:5432/paperclip", + embeddedPostgresDataDir: "/tmp/paperclip-test-db", + embeddedPostgresPort: 54329, + databaseBackupEnabled: false, + databaseBackupIntervalMinutes: 60, + databaseBackupRetentionDays: 30, + databaseBackupDir: "/tmp/paperclip-test-backups", + serveUi: false, + uiDevMiddleware: false, + secretsProvider: "local_encrypted", + secretsStrictMode: false, + secretsMasterKeyFilePath: "/tmp/paperclip-master.key", + storageProvider: "local_disk", + storageLocalDiskBaseDir: "/tmp/paperclip-storage", + storageS3Bucket: "paperclip-test", + storageS3Region: "us-east-1", + storageS3Endpoint: undefined, + storageS3Prefix: "", + storageS3ForcePathStyle: false, + feedbackExportBackendUrl: "https://telemetry.example.com", + feedbackExportBackendToken: "telemetry-token", + heartbeatSchedulerEnabled: false, + heartbeatSchedulerIntervalMs: 30000, + companyDeletionEnabled: false, + })), +})); + +vi.mock("../middleware/logger.js", () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock("../realtime/live-events-ws.js", () => ({ + setupLiveEventsWebSocketServer: vi.fn(), +})); + +vi.mock("../services/index.js", () => ({ + feedbackService: feedbackServiceFactoryMock, + heartbeatService: vi.fn(() => ({ + reapOrphanedRuns: vi.fn(async () => undefined), + resumeQueuedRuns: vi.fn(async () => undefined), + tickTimers: vi.fn(async () => ({ enqueued: 0 })), + })), + reconcilePersistedRuntimeServicesOnStartup: vi.fn(async () => ({ reconciled: 0 })), + routineService: vi.fn(() => ({ + tickScheduledTriggers: vi.fn(async () => ({ triggered: 0 })), + })), +})); + +vi.mock("../storage/index.js", () => ({ + createStorageServiceFromConfig: vi.fn(() => ({ id: "storage-service" })), +})); + +vi.mock("../services/feedback-share-client.js", () => ({ + createFeedbackTraceShareClientFromConfig: vi.fn(() => ({ id: "feedback-share-client" })), +})); + +vi.mock("../startup-banner.js", () => ({ + printStartupBanner: vi.fn(), +})); + +vi.mock("../board-claim.js", () => ({ + getBoardClaimWarningUrl: vi.fn(() => null), + initializeBoardClaimChallenge: vi.fn(async () => undefined), +})); + +vi.mock("../auth/better-auth.js", () => ({ + createBetterAuthHandler: vi.fn(() => undefined), + createBetterAuthInstance: vi.fn(() => ({})), + deriveAuthTrustedOrigins: vi.fn(() => []), + resolveBetterAuthSession: vi.fn(async () => null), + resolveBetterAuthSessionFromHeaders: vi.fn(async () => null), +})); + +import { startServer } from "../index.ts"; + +describe("startServer feedback export wiring", () => { + beforeEach(() => { + vi.clearAllMocks(); + process.env.BETTER_AUTH_SECRET = "test-secret"; + }); + + it("passes the feedback export service into createApp so pending traces flush in runtime", async () => { + const started = await startServer(); + + expect(started.server).toBe(fakeServer); + expect(feedbackServiceFactoryMock).toHaveBeenCalledTimes(1); + expect(createAppMock).toHaveBeenCalledTimes(1); + expect(createAppMock.mock.calls[0]?.[1]).toMatchObject({ + feedbackExportService: feedbackExportServiceMock, + storageService: { id: "storage-service" }, + serverPort: 3210, + }); + }); +}); diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index dc54b810..cfa17e87 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -51,7 +51,7 @@ async function runGit(cwd: string, args: string[]) { await execFileAsync("git", args, { cwd }); } -async function createTempRepo() { +async function createTempRepo(defaultBranch = "main") { const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-repo-")); await runGit(repoRoot, ["init"]); await runGit(repoRoot, ["config", "user.email", "paperclip@example.com"]); @@ -59,7 +59,7 @@ async function createTempRepo() { await fs.writeFile(path.join(repoRoot, "README.md"), "hello\n", "utf8"); await runGit(repoRoot, ["add", "README.md"]); await runGit(repoRoot, ["commit", "-m", "Initial commit"]); - await runGit(repoRoot, ["checkout", "-B", "main"]); + await runGit(repoRoot, ["checkout", "-B", defaultBranch]); return repoRoot; } @@ -658,13 +658,7 @@ describe("realizeExecutionWorkspace", () => { it("auto-detects the default branch when baseRef is not configured", async () => { // Create a repo with "master" as default branch (not "main") - const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-master-")); - await runGit(repoRoot, ["init", "-b", "master"]); - await runGit(repoRoot, ["config", "user.email", "paperclip@example.com"]); - await runGit(repoRoot, ["config", "user.name", "Paperclip Test"]); - await fs.writeFile(path.join(repoRoot, "README.md"), "hello\n", "utf8"); - await runGit(repoRoot, ["add", "README.md"]); - await runGit(repoRoot, ["commit", "-m", "Initial commit"]); + const repoRoot = await createTempRepo("master"); // Set up a bare remote and push master so refs/remotes/origin/master // exists locally. Note: refs/remotes/origin/HEAD is NOT set by a manual @@ -716,13 +710,7 @@ describe("realizeExecutionWorkspace", () => { it("auto-detects the default branch via symbolic-ref when origin/HEAD is set", async () => { // Create a repo with "master" as default branch - const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-symref-")); - await runGit(repoRoot, ["init", "-b", "master"]); - await runGit(repoRoot, ["config", "user.email", "paperclip@example.com"]); - await runGit(repoRoot, ["config", "user.name", "Paperclip Test"]); - await fs.writeFile(path.join(repoRoot, "README.md"), "hello\n", "utf8"); - await runGit(repoRoot, ["add", "README.md"]); - await runGit(repoRoot, ["commit", "-m", "Initial commit"]); + const repoRoot = await createTempRepo("master"); // Set up a bare remote and push const bareRemote = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-bare-symref-")); diff --git a/server/src/app.ts b/server/src/app.ts index 5535ab3d..b9faee2f 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -49,6 +49,7 @@ import { createHostClientHandlers } from "@paperclipai/plugin-sdk"; import type { BetterAuthSessionResult } from "./auth/better-auth.js"; type UiMode = "none" | "static" | "vite-dev"; +const FEEDBACK_EXPORT_FLUSH_INTERVAL_MS = 5_000; export function resolveViteHmrPort(serverPort: number): number { if (serverPort <= 55_535) { @@ -63,6 +64,13 @@ export async function createApp( uiMode: UiMode; serverPort: number; storageService: StorageService; + feedbackExportService?: { + flushPendingFeedbackTraces(input?: { + companyId?: string; + limit?: number; + now?: Date; + }): Promise; + }; deploymentMode: DeploymentMode; deploymentExposure: DeploymentExposure; allowedHostnames: string[]; @@ -288,6 +296,19 @@ export async function createApp( jobCoordinator.start(); scheduler.start(); + const feedbackExportTimer = opts.feedbackExportService + ? setInterval(() => { + void opts.feedbackExportService?.flushPendingFeedbackTraces().catch((err) => { + logger.error({ err }, "Failed to flush pending feedback exports"); + }); + }, FEEDBACK_EXPORT_FLUSH_INTERVAL_MS) + : null; + feedbackExportTimer?.unref?.(); + if (opts.feedbackExportService) { + void opts.feedbackExportService.flushPendingFeedbackTraces().catch((err) => { + logger.error({ err }, "Failed to flush pending feedback exports"); + }); + } void toolDispatcher.initialize().catch((err) => { logger.error({ err }, "Failed to initialize plugin tool dispatcher"); }); @@ -308,6 +329,7 @@ export async function createApp( logger.error({ err }, "Failed to load ready plugins on startup"); }); process.once("exit", () => { + if (feedbackExportTimer) clearInterval(feedbackExportTimer); devWatcher?.close(); hostServiceCleanup.disposeAll(); hostServiceCleanup.teardown(); diff --git a/server/src/config.ts b/server/src/config.ts index 4a1cc17b..33173746 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -70,6 +70,8 @@ export interface Config { storageS3Endpoint: string | undefined; storageS3Prefix: string; storageS3ForcePathStyle: boolean; + feedbackExportBackendUrl: string | undefined; + feedbackExportBackendToken: string | undefined; heartbeatSchedulerEnabled: boolean; heartbeatSchedulerIntervalMs: number; companyDeletionEnabled: boolean; @@ -120,6 +122,14 @@ export function loadConfig(): Config { process.env.PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE !== undefined ? process.env.PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE === "true" : (fileStorage?.s3?.forcePathStyle ?? false); + const feedbackExportBackendUrl = + process.env.PAPERCLIP_FEEDBACK_EXPORT_BACKEND_URL?.trim() || + process.env.PAPERCLIP_TELEMETRY_BACKEND_URL?.trim() || + undefined; + const feedbackExportBackendToken = + process.env.PAPERCLIP_FEEDBACK_EXPORT_BACKEND_TOKEN?.trim() || + process.env.PAPERCLIP_TELEMETRY_BACKEND_TOKEN?.trim() || + undefined; const deploymentModeFromEnvRaw = process.env.PAPERCLIP_DEPLOYMENT_MODE; const deploymentModeFromEnv = @@ -252,6 +262,8 @@ export function loadConfig(): Config { storageS3Endpoint, storageS3Prefix, storageS3ForcePathStyle, + feedbackExportBackendUrl, + feedbackExportBackendToken, heartbeatSchedulerEnabled: process.env.HEARTBEAT_SCHEDULER_ENABLED !== "false", heartbeatSchedulerIntervalMs: Math.max(10000, Number(process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS) || 30000), companyDeletionEnabled, diff --git a/server/src/index.ts b/server/src/index.ts index 7ebfa7d1..23800d95 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -28,7 +28,13 @@ import { createApp } from "./app.js"; import { loadConfig } from "./config.js"; import { logger } from "./middleware/logger.js"; import { setupLiveEventsWebSocketServer } from "./realtime/live-events-ws.js"; -import { heartbeatService, reconcilePersistedRuntimeServicesOnStartup, routineService } from "./services/index.js"; +import { + feedbackService, + heartbeatService, + reconcilePersistedRuntimeServicesOnStartup, + routineService, +} from "./services/index.js"; +import { createFeedbackTraceShareClientFromConfig } from "./services/feedback-share-client.js"; import { createStorageServiceFromConfig } from "./storage/index.js"; import { printStartupBanner } from "./startup-banner.js"; import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js"; @@ -516,10 +522,14 @@ export async function startServer(): Promise { }); const uiMode = config.uiDevMiddleware ? "vite-dev" : config.serveUi ? "static" : "none"; const storageService = createStorageServiceFromConfig(config); + const feedback = feedbackService(db as any, { + shareClient: createFeedbackTraceShareClientFromConfig(config) ?? undefined, + }); const app = await createApp(db as any, { uiMode, serverPort: listenPort, storageService, + feedbackExportService: feedback, deploymentMode: config.deploymentMode, deploymentExposure: config.deploymentExposure, allowedHostnames: config.allowedHostnames, diff --git a/server/src/routes/companies.ts b/server/src/routes/companies.ts index 5112baac..d1978c9d 100644 --- a/server/src/routes/companies.ts +++ b/server/src/routes/companies.ts @@ -1,14 +1,18 @@ import { Router, type Request } from "express"; import type { Db } from "@paperclipai/db"; import { + DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION, companyPortabilityExportSchema, companyPortabilityImportSchema, companyPortabilityPreviewSchema, createCompanySchema, + feedbackTargetTypeSchema, + feedbackTraceStatusSchema, + feedbackVoteValueSchema, updateCompanyBrandingSchema, updateCompanySchema, } from "@paperclipai/shared"; -import { forbidden } from "../errors.js"; +import { badRequest, forbidden } from "../errors.js"; import { validate } from "../middleware/validate.js"; import { accessService, @@ -16,6 +20,7 @@ import { budgetService, companyPortabilityService, companyService, + feedbackService, logActivity, } from "../services/index.js"; import type { StorageService } from "../storage/types.js"; @@ -28,6 +33,20 @@ export function companyRoutes(db: Db, storage?: StorageService) { const portability = companyPortabilityService(db, storage); const access = accessService(db); const budgets = budgetService(db); + const feedback = feedbackService(db); + + function parseBooleanQuery(value: unknown) { + return value === true || value === "true" || value === "1"; + } + + function parseDateQuery(value: unknown, field: string) { + if (typeof value !== "string" || value.trim().length === 0) return undefined; + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + throw badRequest(`Invalid ${field} query value`); + } + return parsed; + } async function assertCanUpdateBranding(req: Request, companyId: string) { assertCompanyAccess(req, companyId); @@ -104,6 +123,34 @@ export function companyRoutes(db: Db, storage?: StorageService) { res.json(company); }); + router.get("/:companyId/feedback-traces", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + assertBoard(req); + + const targetTypeRaw = typeof req.query.targetType === "string" ? req.query.targetType : undefined; + const voteRaw = typeof req.query.vote === "string" ? req.query.vote : undefined; + const statusRaw = typeof req.query.status === "string" ? req.query.status : undefined; + const issueId = typeof req.query.issueId === "string" && req.query.issueId.trim().length > 0 ? req.query.issueId : undefined; + const projectId = typeof req.query.projectId === "string" && req.query.projectId.trim().length > 0 + ? req.query.projectId + : undefined; + + const traces = await feedback.listFeedbackTraces({ + companyId, + issueId, + projectId, + targetType: targetTypeRaw ? feedbackTargetTypeSchema.parse(targetTypeRaw) : undefined, + vote: voteRaw ? feedbackVoteValueSchema.parse(voteRaw) : undefined, + status: statusRaw ? feedbackTraceStatusSchema.parse(statusRaw) : undefined, + from: parseDateQuery(req.query.from, "from"), + to: parseDateQuery(req.query.to, "to"), + sharedOnly: parseBooleanQuery(req.query.sharedOnly), + includePayload: parseBooleanQuery(req.query.includePayload), + }); + res.json(traces); + }); + router.post("/:companyId/export", validate(companyPortabilityExportSchema), async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); @@ -246,6 +293,11 @@ export function companyRoutes(db: Db, storage?: StorageService) { assertCompanyAccess(req, companyId); const actor = getActorInfo(req); + const existingCompany = await svc.getById(companyId); + if (!existingCompany) { + res.status(404).json({ error: "Company not found" }); + return; + } let body: Record; if (req.actor.type === "agent") { @@ -262,6 +314,18 @@ export function companyRoutes(db: Db, storage?: StorageService) { } else { assertBoard(req); body = updateCompanySchema.parse(req.body); + + if (body.feedbackDataSharingEnabled === true && !existingCompany.feedbackDataSharingEnabled) { + body = { + ...body, + feedbackDataSharingConsentAt: new Date(), + feedbackDataSharingConsentByUserId: req.actor.userId ?? "local-board", + feedbackDataSharingTermsVersion: + typeof body.feedbackDataSharingTermsVersion === "string" && body.feedbackDataSharingTermsVersion.length > 0 + ? body.feedbackDataSharingTermsVersion + : DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION, + }; + } } const company = await svc.update(companyId, body); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index d07de42a..459cb396 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -9,6 +9,10 @@ import { createIssueLabelSchema, checkoutIssueSchema, createIssueSchema, + feedbackTargetTypeSchema, + feedbackTraceStatusSchema, + feedbackVoteValueSchema, + upsertIssueFeedbackVoteSchema, linkIssueApprovalSchema, issueDocumentKeySchema, restoreIssueDocumentRevisionSchema, @@ -22,8 +26,10 @@ import { accessService, agentService, executionWorkspaceService, + feedbackService, goalService, heartbeatService, + instanceSettingsService, issueApprovalService, issueService, documentService, @@ -49,6 +55,8 @@ export function issueRoutes(db: Db, storage: StorageService) { const svc = issueService(db); const access = accessService(db); const heartbeat = heartbeatService(db); + const feedback = feedbackService(db); + const instanceSettings = instanceSettingsService(db); const agentsSvc = agentService(db); const projectsSvc = projectService(db); const goalsSvc = goalService(db); @@ -69,6 +77,19 @@ export function issueRoutes(db: Db, storage: StorageService) { }; } + function parseBooleanQuery(value: unknown) { + return value === true || value === "true" || value === "1"; + } + + function parseDateQuery(value: unknown, field: string) { + if (typeof value !== "string" || value.trim().length === 0) return undefined; + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + throw new HttpError(400, `Invalid ${field} query value`); + } + return parsed; + } + async function runSingleFileUpload(req: Request, res: Response) { await new Promise((resolve, reject) => { upload.single("file")(req, res, (err: unknown) => { @@ -542,6 +563,7 @@ export function issueRoutes(db: Db, storage: StorageService) { baseRevisionId: req.body.baseRevisionId ?? null, createdByAgentId: actor.agentId ?? null, createdByUserId: actor.actorType === "user" ? actor.actorId : null, + createdByRunId: actor.runId ?? null, }); const doc = result.document; @@ -1153,6 +1175,7 @@ export function issueRoutes(db: Db, storage: StorageService) { comment = await svc.addComment(id, commentBody, { agentId: actor.agentId ?? undefined, userId: actor.actorType === "user" ? actor.actorId : undefined, + runId: actor.runId, }); await logActivity(db, { @@ -1462,6 +1485,87 @@ export function issueRoutes(db: Db, storage: StorageService) { res.json(comment); }); + router.get("/issues/:id/feedback-votes", async (req, res) => { + const id = req.params.id as string; + const issue = await svc.getById(id); + if (!issue) { + res.status(404).json({ error: "Issue not found" }); + return; + } + assertCompanyAccess(req, issue.companyId); + if (req.actor.type !== "board") { + res.status(403).json({ error: "Only board users can view feedback votes" }); + return; + } + + const votes = await feedback.listIssueVotesForUser(id, req.actor.userId ?? "local-board"); + res.json(votes); + }); + + router.get("/issues/:id/feedback-traces", async (req, res) => { + const id = req.params.id as string; + const issue = await svc.getById(id); + if (!issue) { + res.status(404).json({ error: "Issue not found" }); + return; + } + assertCompanyAccess(req, issue.companyId); + if (req.actor.type !== "board") { + res.status(403).json({ error: "Only board users can view feedback traces" }); + return; + } + + const targetTypeRaw = typeof req.query.targetType === "string" ? req.query.targetType : undefined; + const voteRaw = typeof req.query.vote === "string" ? req.query.vote : undefined; + const statusRaw = typeof req.query.status === "string" ? req.query.status : undefined; + const targetType = targetTypeRaw ? feedbackTargetTypeSchema.parse(targetTypeRaw) : undefined; + const vote = voteRaw ? feedbackVoteValueSchema.parse(voteRaw) : undefined; + const status = statusRaw ? feedbackTraceStatusSchema.parse(statusRaw) : undefined; + + const traces = await feedback.listFeedbackTraces({ + companyId: issue.companyId, + issueId: issue.id, + targetType, + vote, + status, + from: parseDateQuery(req.query.from, "from"), + to: parseDateQuery(req.query.to, "to"), + sharedOnly: parseBooleanQuery(req.query.sharedOnly), + includePayload: parseBooleanQuery(req.query.includePayload), + }); + res.json(traces); + }); + + router.get("/feedback-traces/:traceId", async (req, res) => { + const traceId = req.params.traceId as string; + const trace = await feedback.getFeedbackTraceById(traceId, parseBooleanQuery(req.query.includePayload) || req.query.includePayload === undefined); + if (!trace) { + res.status(404).json({ error: "Feedback trace not found" }); + return; + } + assertCompanyAccess(req, trace.companyId); + if (req.actor.type !== "board") { + res.status(403).json({ error: "Only board users can view feedback traces" }); + return; + } + res.json(trace); + }); + + router.get("/feedback-traces/:traceId/bundle", async (req, res) => { + const traceId = req.params.traceId as string; + const bundle = await feedback.getFeedbackTraceBundle(traceId); + if (!bundle) { + res.status(404).json({ error: "Feedback trace not found" }); + return; + } + assertCompanyAccess(req, bundle.companyId); + if (req.actor.type !== "board") { + res.status(403).json({ error: "Only board users can view feedback trace bundles" }); + return; + } + res.json(bundle); + }); + router.post("/issues/:id/comments", validate(addIssueCommentSchema), async (req, res) => { const id = req.params.id as string; const issue = await svc.getById(id); @@ -1539,6 +1643,7 @@ export function issueRoutes(db: Db, storage: StorageService) { const comment = await svc.addComment(id, req.body.body, { agentId: actor.agentId ?? undefined, userId: actor.actorType === "user" ? actor.actorId : undefined, + runId: actor.runId, }); if (actor.runId) { @@ -1660,6 +1765,93 @@ export function issueRoutes(db: Db, storage: StorageService) { res.status(201).json(comment); }); + router.post("/issues/:id/feedback-votes", validate(upsertIssueFeedbackVoteSchema), async (req, res) => { + const id = req.params.id as string; + const issue = await svc.getById(id); + if (!issue) { + res.status(404).json({ error: "Issue not found" }); + return; + } + assertCompanyAccess(req, issue.companyId); + if (req.actor.type !== "board") { + res.status(403).json({ error: "Only board users can vote on AI feedback" }); + return; + } + + const actor = getActorInfo(req); + const result = await feedback.saveIssueVote({ + issueId: id, + targetType: req.body.targetType, + targetId: req.body.targetId, + vote: req.body.vote, + reason: req.body.reason, + authorUserId: req.actor.userId ?? "local-board", + allowSharing: req.body.allowSharing === true, + }); + + await logActivity(db, { + companyId: issue.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "issue.feedback_vote_saved", + entityType: "issue", + entityId: issue.id, + details: { + identifier: issue.identifier, + targetType: result.vote.targetType, + targetId: result.vote.targetId, + vote: result.vote.vote, + hasReason: Boolean(result.vote.reason), + sharingEnabled: result.sharingEnabled, + }, + }); + + if (result.consentEnabledNow) { + await logActivity(db, { + companyId: issue.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "company.feedback_data_sharing_updated", + entityType: "company", + entityId: issue.companyId, + details: { + feedbackDataSharingEnabled: true, + source: "issue_feedback_vote", + }, + }); + } + + if (result.persistedSharingPreference) { + const settings = await instanceSettings.get(); + const companyIds = await instanceSettings.listCompanyIds(); + await Promise.all( + companyIds.map((companyId) => + logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "instance.settings.general_updated", + entityType: "instance_settings", + entityId: settings.id, + details: { + general: settings.general, + changedKeys: ["feedbackDataSharingPreference"], + source: "issue_feedback_vote", + }, + }), + ), + ); + } + + res.status(201).json(result.vote); + }); + router.get("/issues/:id/attachments", async (req, res) => { const issueId = req.params.id as string; const issue = await svc.getById(issueId); diff --git a/server/src/services/companies.ts b/server/src/services/companies.ts index 893bea9e..1d23dab2 100644 --- a/server/src/services/companies.ts +++ b/server/src/services/companies.ts @@ -41,6 +41,10 @@ export function companyService(db: Db) { budgetMonthlyCents: companies.budgetMonthlyCents, spentMonthlyCents: companies.spentMonthlyCents, requireBoardApprovalForNewAgents: companies.requireBoardApprovalForNewAgents, + feedbackDataSharingEnabled: companies.feedbackDataSharingEnabled, + feedbackDataSharingConsentAt: companies.feedbackDataSharingConsentAt, + feedbackDataSharingConsentByUserId: companies.feedbackDataSharingConsentByUserId, + feedbackDataSharingTermsVersion: companies.feedbackDataSharingTermsVersion, brandColor: companies.brandColor, logoAssetId: companyLogos.assetId, createdAt: companies.createdAt, diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index b1bb7ed9..0818b431 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -2289,7 +2289,7 @@ function buildManifestFromPackageFiles( const skillPaths = Array.from(new Set([...referencedSkillPaths, ...discoveredSkillPaths])).sort(); const manifest: CompanyPortabilityManifest = { - schemaVersion: 4, + schemaVersion: 5, generatedAt: new Date().toISOString(), source: opts?.sourceLabel ?? null, includes: { @@ -2309,6 +2309,18 @@ function buildManifestFromPackageFiles( typeof paperclipCompany.requireBoardApprovalForNewAgents === "boolean" ? paperclipCompany.requireBoardApprovalForNewAgents : readCompanyApprovalDefault(companyFrontmatter), + feedbackDataSharingEnabled: + typeof paperclipCompany.feedbackDataSharingEnabled === "boolean" + ? paperclipCompany.feedbackDataSharingEnabled + : false, + feedbackDataSharingConsentAt: + typeof paperclipCompany.feedbackDataSharingConsentAt === "string" + ? paperclipCompany.feedbackDataSharingConsentAt + : null, + feedbackDataSharingConsentByUserId: + asString(paperclipCompany.feedbackDataSharingConsentByUserId), + feedbackDataSharingTermsVersion: + asString(paperclipCompany.feedbackDataSharingTermsVersion), }, sidebar: paperclipSidebar, agents: [], @@ -3227,6 +3239,10 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { brandColor: company.brandColor ?? null, logoPath: companyLogoPath, requireBoardApprovalForNewAgents: company.requireBoardApprovalForNewAgents ? undefined : false, + feedbackDataSharingEnabled: company.feedbackDataSharingEnabled ? true : undefined, + feedbackDataSharingConsentAt: company.feedbackDataSharingConsentAt?.toISOString() ?? null, + feedbackDataSharingConsentByUserId: company.feedbackDataSharingConsentByUserId ?? null, + feedbackDataSharingTermsVersion: company.feedbackDataSharingTermsVersion ?? null, }), sidebar: stripEmptyValues(sidebarOrder), agents: Object.keys(paperclipAgents).length > 0 ? paperclipAgents : undefined, @@ -3736,6 +3752,18 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { requireBoardApprovalForNewAgents: include.company ? (sourceManifest.company?.requireBoardApprovalForNewAgents ?? true) : true, + feedbackDataSharingEnabled: include.company + ? (sourceManifest.company?.feedbackDataSharingEnabled ?? false) + : false, + feedbackDataSharingConsentAt: include.company && sourceManifest.company?.feedbackDataSharingConsentAt + ? new Date(sourceManifest.company.feedbackDataSharingConsentAt) + : null, + feedbackDataSharingConsentByUserId: include.company + ? (sourceManifest.company?.feedbackDataSharingConsentByUserId ?? null) + : null, + feedbackDataSharingTermsVersion: include.company + ? (sourceManifest.company?.feedbackDataSharingTermsVersion ?? null) + : null, }); if (mode === "agent_safe" && options?.sourceCompanyId) { await access.copyActiveUserMemberships(options.sourceCompanyId, created.id); @@ -3753,6 +3781,12 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { description: sourceManifest.company.description, brandColor: sourceManifest.company.brandColor, requireBoardApprovalForNewAgents: sourceManifest.company.requireBoardApprovalForNewAgents, + feedbackDataSharingEnabled: sourceManifest.company.feedbackDataSharingEnabled, + feedbackDataSharingConsentAt: sourceManifest.company.feedbackDataSharingConsentAt + ? new Date(sourceManifest.company.feedbackDataSharingConsentAt) + : null, + feedbackDataSharingConsentByUserId: sourceManifest.company.feedbackDataSharingConsentByUserId, + feedbackDataSharingTermsVersion: sourceManifest.company.feedbackDataSharingTermsVersion, }); targetCompany = updated ?? targetCompany; companyAction = "updated"; diff --git a/server/src/services/documents.ts b/server/src/services/documents.ts index b5152f5a..1a56af27 100644 --- a/server/src/services/documents.ts +++ b/server/src/services/documents.ts @@ -171,6 +171,7 @@ export function documentService(db: Db) { baseRevisionId?: string | null; createdByAgentId?: string | null; createdByUserId?: string | null; + createdByRunId?: string | null; }) => { const key = normalizeDocumentKey(input.key); const issue = await db @@ -231,6 +232,7 @@ export function documentService(db: Db) { changeSummary: input.changeSummary ?? null, createdByAgentId: input.createdByAgentId ?? null, createdByUserId: input.createdByUserId ?? null, + createdByRunId: input.createdByRunId ?? null, createdAt: now, }) .returning(); @@ -304,6 +306,7 @@ export function documentService(db: Db) { changeSummary: input.changeSummary ?? null, createdByAgentId: input.createdByAgentId ?? null, createdByUserId: input.createdByUserId ?? null, + createdByRunId: input.createdByRunId ?? null, createdAt: now, }) .returning(); diff --git a/server/src/services/feedback-redaction.ts b/server/src/services/feedback-redaction.ts new file mode 100644 index 00000000..7c0ed097 --- /dev/null +++ b/server/src/services/feedback-redaction.ts @@ -0,0 +1,193 @@ +import { createHash } from "node:crypto"; +import { redactCurrentUserText } from "../log-redaction.js"; +import { sanitizeRecord } from "../redaction.js"; + +export type FeedbackRedactionState = { + redactedFields: Set; + truncatedFields: Set; + omittedFields: Set; + notes: Set; + counts: Map; +}; + +type PatternReplacement = string | ((match: string, ...args: string[]) => string); + +type RedactionPattern = { + kind: string; + regex: RegExp; + replacement: PatternReplacement; +}; + +const SECRET_ASSIGNMENT_RE = + /\b(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)\s*[:=]\s*([^\s,;]+)/gi; + +const FREE_TEXT_PATTERNS: RedactionPattern[] = [ + { + kind: "pem_block", + regex: /-----BEGIN [^-]+-----[\s\S]+?-----END [^-]+-----/g, + replacement: "[REDACTED_PEM_BLOCK]", + }, + { + kind: "secret_assignment", + regex: SECRET_ASSIGNMENT_RE, + replacement: (_match, key: string) => `${key}=[REDACTED]`, + }, + { + kind: "bearer_token", + regex: /Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, + replacement: "Bearer [REDACTED_TOKEN]", + }, + { + kind: "github_token", + regex: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, + replacement: "[REDACTED_GITHUB_TOKEN]", + }, + { + kind: "provider_api_key", + regex: /\bsk-(?:ant-)?[A-Za-z0-9_-]{12,}\b/g, + replacement: "[REDACTED_API_KEY]", + }, + { + kind: "jwt", + regex: /\b[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?\b/g, + replacement: "[REDACTED_JWT]", + }, + { + kind: "dsn", + regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|kafka|nats|mssql):\/\/[^\s<>'")]+/gi, + replacement: "[REDACTED_CONNECTION_STRING]", + }, + { + kind: "email", + regex: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, + replacement: "[REDACTED_EMAIL]", + }, + { + kind: "phone", + regex: /(? { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function increment(state: FeedbackRedactionState, kind: string, count: number) { + if (count <= 0) return; + state.counts.set(kind, (state.counts.get(kind) ?? 0) + count); +} + +function recordField(state: FeedbackRedactionState, fieldPath: string) { + if (fieldPath.trim().length === 0) return; + state.redactedFields.add(fieldPath); +} + +function applyPattern(input: string, pattern: RedactionPattern) { + const matches = Array.from(input.matchAll(pattern.regex)).length; + if (matches === 0) { + pattern.regex.lastIndex = 0; + return { output: input, matches: 0 }; + } + const output = input.replace(pattern.regex, pattern.replacement as never); + pattern.regex.lastIndex = 0; + return { output, matches }; +} + +export function createFeedbackRedactionState(): FeedbackRedactionState { + return { + redactedFields: new Set(), + truncatedFields: new Set(), + omittedFields: new Set(), + notes: new Set(), + counts: new Map(), + }; +} + +export function sanitizeFeedbackText( + input: string, + state: FeedbackRedactionState, + fieldPath: string, + maxLength: number, +) { + let output = redactCurrentUserText(input); + if (output !== input) { + recordField(state, fieldPath); + increment(state, "current_user", 1); + } + + for (const pattern of FREE_TEXT_PATTERNS) { + const result = applyPattern(output, pattern); + if (result.matches > 0) { + output = result.output; + recordField(state, fieldPath); + increment(state, pattern.kind, result.matches); + } + } + + if (output.length > maxLength) { + output = `${output.slice(0, Math.max(0, maxLength - 1))}...`; + state.truncatedFields.add(fieldPath); + } + + return output; +} + +export function sanitizeFeedbackValue( + value: unknown, + state: FeedbackRedactionState, + fieldPath: string, + maxStringLength: number, +): unknown { + if (typeof value === "string") { + return sanitizeFeedbackText(value, state, fieldPath, maxStringLength); + } + if (Array.isArray(value)) { + return value.map((entry, index) => + sanitizeFeedbackValue(entry, state, `${fieldPath}[${index}]`, maxStringLength)); + } + if (!isPlainRecord(value)) { + return value; + } + + const structurallySanitized = sanitizeRecord(value); + if (stableStringify(structurallySanitized) !== stableStringify(value)) { + recordField(state, fieldPath); + increment(state, "structured_secret", 1); + } + + const output: Record = {}; + for (const [key, entry] of Object.entries(structurallySanitized)) { + output[key] = sanitizeFeedbackValue(entry, state, `${fieldPath}.${key}`, maxStringLength); + } + return output; +} + +export function finalizeFeedbackRedactionSummary(state: FeedbackRedactionState) { + return { + strategy: "deterministic_feedback_v2", + redactedFields: Array.from(state.redactedFields).sort(), + truncatedFields: Array.from(state.truncatedFields).sort(), + omittedFields: Array.from(state.omittedFields).sort(), + notes: Array.from(state.notes).sort(), + counts: Object.fromEntries(Array.from(state.counts.entries()).sort(([left], [right]) => left.localeCompare(right))), + } satisfies Record; +} + +export function stableStringify(value: unknown): string { + if (value === null || typeof value !== "object") { + return JSON.stringify(value); + } + if (Array.isArray(value)) { + return `[${value.map((entry) => stableStringify(entry)).join(",")}]`; + } + + const entries = Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`); + return `{${entries.join(",")}}`; +} + +export function sha256Digest(value: unknown) { + return createHash("sha256").update(stableStringify(value)).digest("hex"); +} diff --git a/server/src/services/feedback-share-client.ts b/server/src/services/feedback-share-client.ts new file mode 100644 index 00000000..e1761b14 --- /dev/null +++ b/server/src/services/feedback-share-client.ts @@ -0,0 +1,54 @@ +import type { FeedbackTraceBundle } from "@paperclipai/shared"; +import type { Config } from "../config.js"; + +function buildFeedbackShareObjectKey(bundle: FeedbackTraceBundle, exportedAt: Date) { + const year = String(exportedAt.getUTCFullYear()); + const month = String(exportedAt.getUTCMonth() + 1).padStart(2, "0"); + const day = String(exportedAt.getUTCDate()).padStart(2, "0"); + return `feedback-traces/${bundle.companyId}/${year}/${month}/${day}/${bundle.exportId ?? bundle.traceId}.json`; +} + +export interface FeedbackTraceShareClient { + uploadTraceBundle(bundle: FeedbackTraceBundle): Promise<{ objectKey: string }>; +} + +export function createFeedbackTraceShareClientFromConfig( + config: Pick, +): FeedbackTraceShareClient | null { + const baseUrl = config.feedbackExportBackendUrl?.trim(); + if (!baseUrl) return null; + + const token = config.feedbackExportBackendToken?.trim(); + const endpoint = new URL("/feedback-traces", baseUrl).toString(); + + return { + async uploadTraceBundle(bundle) { + const exportedAt = new Date(); + const objectKey = buildFeedbackShareObjectKey(bundle, exportedAt); + const response = await fetch(endpoint, { + method: "POST", + headers: { + "content-type": "application/json", + ...(token ? { authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + objectKey, + exportedAt: exportedAt.toISOString(), + bundle, + }), + }); + + if (!response.ok) { + const detail = await response.text().catch(() => ""); + throw new Error(detail.trim() || `Feedback trace upload failed with HTTP ${response.status}`); + } + + const payload = await response.json().catch(() => null) as { objectKey?: unknown } | null; + return { + objectKey: typeof payload?.objectKey === "string" && payload.objectKey.trim().length > 0 + ? payload.objectKey + : objectKey, + }; + }, + }; +} diff --git a/server/src/services/feedback.ts b/server/src/services/feedback.ts new file mode 100644 index 00000000..57175ce9 --- /dev/null +++ b/server/src/services/feedback.ts @@ -0,0 +1,2045 @@ +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import { and, asc, desc, eq, getTableColumns, gte, lte, ne, or } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { + agents, + companies, + companySkills, + costEvents, + documentRevisions, + documents, + feedbackExports, + feedbackVotes, + heartbeatRunEvents, + heartbeatRuns, + instanceSettings, + issueComments, + issueDocuments, + issues, +} from "@paperclipai/db"; +import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; +import { claudeConfigDir, parseClaudeStreamJson } from "@paperclipai/adapter-claude-local/server"; +import { codexHomeDir, parseCodexJsonl } from "@paperclipai/adapter-codex-local/server"; +import { parseOpenCodeJsonl } from "@paperclipai/adapter-opencode-local/server"; +import { + DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, + DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION, + instanceGeneralSettingsSchema, + type FeedbackTargetType, + type FeedbackTraceBundle, + type FeedbackTraceBundleCaptureStatus, + type FeedbackTraceBundleFile, + type FeedbackTrace, + type FeedbackTraceStatus, + type FeedbackTraceTargetSummary, + type FeedbackVoteValue, +} from "@paperclipai/shared"; +import { resolveHomeAwarePath, resolvePaperclipInstanceRoot } from "../home-paths.js"; +import { notFound, unprocessable } from "../errors.js"; +import { agentInstructionsService } from "./agent-instructions.js"; +import { + createFeedbackRedactionState, + finalizeFeedbackRedactionSummary, + sanitizeFeedbackText, + sanitizeFeedbackValue, + sha256Digest, +} from "./feedback-redaction.js"; +import { getRunLogStore } from "./run-log-store.js"; + +const FEEDBACK_SCHEMA_VERSION = "paperclip-feedback-envelope-v2"; +const FEEDBACK_BUNDLE_VERSION = "paperclip-feedback-bundle-v2"; +const FEEDBACK_DESTINATION = "paperclip_labs_feedback_v1"; +const FEEDBACK_CONTEXT_WINDOW = 3; +const MAX_EXCERPT_CHARS = 200; +const MAX_PRIMARY_CONTENT_CHARS = 8_000; +const MAX_CONTEXT_ITEM_BODY_CHARS = 3_000; +const MAX_TOTAL_CONTEXT_CHARS = 12_000; +const MAX_DESCRIPTION_CHARS = 1_200; +const MAX_INSTRUCTIONS_BODY_CHARS = 8_000; +const MAX_PATH_CHARS = 600; +const MAX_SKILLS = 20; +const MAX_INSTRUCTION_FILES = 20; +const MAX_TRACE_FILE_CHARS = 10_000_000; +const DEFAULT_INSTANCE_SETTINGS_SINGLETON_KEY = "default"; + +type FeedbackTraceRow = typeof feedbackExports.$inferSelect & { + issueIdentifier: string | null; + issueTitle: string; +}; + +type PendingFeedbackExportRow = typeof feedbackExports.$inferSelect; + +type IssueFeedbackContext = { + id: string; + companyId: string; + projectId: string | null; + identifier: string | null; + title: string; + description: string | null; +}; + +type FeedbackTargetRecord = { + targetType: FeedbackTargetType; + targetId: string; + label: string; + body: string; + createdAt: Date; + authorAgentId: string | null; + authorUserId: string | null; + createdByRunId: string | null; + documentId: string | null; + documentKey: string | null; + documentTitle: string | null; + revisionNumber: number | null; + issuePath: string | null; + targetPath: string | null; +}; + +type ResolvedFeedbackTarget = FeedbackTargetRecord & { + payloadTarget: Record; +}; + +const feedbackExportColumns = getTableColumns(feedbackExports); +const instructionsSvc = agentInstructionsService(); + +type FeedbackTraceShareClient = { + uploadTraceBundle(bundle: FeedbackTraceBundle): Promise<{ objectKey: string }>; +}; + +type FeedbackServiceOptions = { + shareClient?: FeedbackTraceShareClient; +}; + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; +} + +function asString(value: unknown) { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function asNumber(value: unknown) { + if (typeof value !== "number" || !Number.isFinite(value)) return null; + return value; +} + +function asBoolean(value: unknown) { + return typeof value === "boolean" ? value : null; +} + +function uniqueNonEmpty(values: Array) { + return Array.from(new Set(values.map((value) => value?.trim() ?? "").filter(Boolean))); +} + +function truncateExcerpt(text: string, max = MAX_EXCERPT_CHARS) { + const normalized = text.replace(/\s+/g, " ").trim(); + if (!normalized) return null; + return normalized.length <= max ? normalized : `${normalized.slice(0, max - 1)}...`; +} + +function contentTypeForPath(filePath: string) { + const lower = filePath.toLowerCase(); + if (lower.endsWith(".jsonl") || lower.endsWith(".ndjson")) return "application/x-ndjson"; + if (lower.endsWith(".json")) return "application/json"; + if (lower.endsWith(".md")) return "text/markdown; charset=utf-8"; + return "text/plain; charset=utf-8"; +} + +function normalizeInstanceGeneralSettings(raw: unknown) { + const parsed = instanceGeneralSettingsSchema.safeParse(raw ?? {}); + if (parsed.success) return parsed.data; + return { + censorUsernameInLogs: false, + feedbackDataSharingPreference: DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, + }; +} + +function buildIssuePath(identifier: string | null) { + if (!identifier) return null; + const prefix = identifier.split("-")[0]?.trim(); + if (!prefix) return null; + return `/${prefix}/issues/${identifier}`; +} + +function buildTargetSummary(input: { + label: string; + excerpt: string | null; + authorAgentId: string | null; + authorUserId: string | null; + createdAt: Date | null; + documentKey?: string | null; + documentTitle?: string | null; + revisionNumber?: number | null; +}): FeedbackTraceTargetSummary { + return { + label: input.label, + excerpt: input.excerpt, + authorAgentId: input.authorAgentId, + authorUserId: input.authorUserId, + createdAt: input.createdAt, + documentKey: input.documentKey ?? null, + documentTitle: input.documentTitle ?? null, + revisionNumber: input.revisionNumber ?? null, + }; +} + +function normalizeReason(vote: FeedbackVoteValue, reason: string | null | undefined) { + if (vote !== "down" || typeof reason !== "string") return null; + const trimmed = reason.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function normalizeSkillReference(value: string) { + return value.trim().toLowerCase(); +} + +function matchesSkillReference( + skill: typeof companySkills.$inferSelect, + reference: string, +) { + const normalized = normalizeSkillReference(reference); + if (!normalized) return false; + if (skill.key.toLowerCase() === normalized) return true; + if (skill.slug.toLowerCase() === normalized) return true; + if (skill.name.toLowerCase() === normalized) return true; + const keyTail = skill.key.split("/").pop()?.toLowerCase(); + return keyTail === normalized; +} + +function buildExportId(feedbackVoteId: string, sharedAt: Date) { + return `fbexp_${sha256Digest(`${feedbackVoteId}:${sharedAt.toISOString()}`).slice(0, 24)}`; +} + +function resolveSourceRunId(payloadSnapshot: Record | null) { + const targetRunId = asString(asRecord(payloadSnapshot?.target)?.createdByRunId); + if (targetRunId) return targetRunId; + + const bundle = asRecord(payloadSnapshot?.bundle); + const agentContext = asRecord(bundle?.agentContext); + const runtime = asRecord(agentContext?.runtime); + return asString(asRecord(runtime?.sourceRun)?.id); +} + +function makeBundleFile(input: { + path: string; + contentType: string; + source: FeedbackTraceBundleFile["source"]; + contents: string; +}) { + return { + path: input.path, + contentType: input.contentType, + encoding: "utf8" as const, + byteLength: Buffer.byteLength(input.contents, "utf8"), + sha256: sha256Digest(input.contents), + source: input.source, + contents: input.contents, + } satisfies FeedbackTraceBundleFile; +} + +function appendNote(notes: string[], note: string) { + if (note.trim().length === 0 || notes.includes(note)) return; + notes.push(note); +} + +async function readTextFileIfPresent( + filePath: string | null, + state: ReturnType, + fieldPath: string, +) { + if (!filePath) return null; + const raw = await readFile(filePath, "utf8").catch(() => null); + if (raw == null) return null; + return sanitizeFeedbackText(raw, state, fieldPath, MAX_TRACE_FILE_CHARS); +} + +async function listChildFiles(dirPath: string) { + const entries = await readdir(dirPath, { withFileTypes: true }).catch(() => []); + return entries + .filter((entry) => entry.isFile()) + .map((entry) => path.join(dirPath, entry.name)) + .sort((left, right) => left.localeCompare(right)); +} + +async function listNestedFiles(dirPath: string, maxDepth = 4): Promise { + async function walk(currentPath: string, depth: number): Promise { + const entries = await readdir(currentPath, { withFileTypes: true }).catch(() => []); + const files = entries + .filter((entry) => entry.isFile()) + .map((entry) => path.join(currentPath, entry.name)) + .sort((left, right) => left.localeCompare(right)); + if (depth >= maxDepth) return files; + + const childDirs = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(currentPath, entry.name)) + .sort((left, right) => left.localeCompare(right)); + const nested = await Promise.all(childDirs.map((childDir) => walk(childDir, depth + 1))); + return [...files, ...nested.flat()]; + } + + return walk(dirPath, 0); +} + +async function findMatchingFile( + rootDir: string, + matcher: (absolutePath: string, name: string) => boolean, + maxDepth = 5, +): Promise { + async function search(dirPath: string, depth: number): Promise { + const entries = await readdir(dirPath, { withFileTypes: true }).catch(() => []); + for (const entry of entries) { + const absolutePath = path.join(dirPath, entry.name); + if (entry.isFile() && matcher(absolutePath, entry.name)) { + return absolutePath; + } + } + if (depth >= maxDepth) return null; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const found = await search(path.join(dirPath, entry.name), depth + 1); + if (found) return found; + } + return null; + } + + return search(rootDir, 0); +} + +async function readFullRunLog(run: { + logStore: string | null; + logRef: string | null; +}) { + if (run.logStore !== "local_file" || !run.logRef) return null; + const store = getRunLogStore(); + let offset = 0; + let combined = ""; + + while (true) { + const result = await store.read({ store: "local_file", logRef: run.logRef }, { + offset, + limitBytes: 512_000, + }).catch(() => null); + if (!result) return combined || null; + combined += result.content; + if (result.nextOffset == null) break; + offset = result.nextOffset; + } + + return combined || null; +} + +function parseRunLogEntries(logText: string | null) { + if (!logText) return []; + const entries: Array<{ ts: string; stream: string; chunk: string }> = []; + for (const rawLine of logText.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line) continue; + try { + const parsed = JSON.parse(line) as { ts?: unknown; stream?: unknown; chunk?: unknown }; + const ts = asString(parsed.ts) ?? new Date(0).toISOString(); + const stream = asString(parsed.stream) ?? "stdout"; + const chunk = typeof parsed.chunk === "string" ? parsed.chunk : ""; + entries.push({ ts, stream, chunk }); + } catch { + // Keep malformed lines out of the normalized bundle but preserve the raw log file separately. + } + } + return entries; +} + +function captureStatusFromFiles(files: FeedbackTraceBundleFile[]): FeedbackTraceBundleCaptureStatus { + const sources = new Set(files.map((file) => file.source)); + if (sources.has("codex_session")) return "full"; + if (sources.has("claude_project_session") || sources.has("claude_debug_log")) return "full"; + if ( + sources.has("opencode_session") && + sources.has("opencode_message") && + sources.has("opencode_message_part") + ) { + return "full"; + } + + const hasAdapterFiles = files.some((file) => + file.source !== "paperclip_run" && + file.source !== "paperclip_run_events" && + file.source !== "paperclip_run_log", + ); + if (hasAdapterFiles) return "partial"; + return files.length > 0 ? "partial" : "unavailable"; +} + +async function buildCodexTraceFiles(input: { + companyId: string; + sessionId: string | null; + state: ReturnType; + notes: string[]; +}) { + const files: FeedbackTraceBundleFile[] = []; + if (!input.sessionId) { + appendNote(input.notes, "codex_session_id_missing"); + return { files, raw: null as Record | null, normalized: null as Record | null }; + } + + const managedRoot = path.join( + resolvePaperclipInstanceRoot(), + "companies", + input.companyId, + "codex-home", + "sessions", + ); + const sharedRoot = path.join(codexHomeDir(), "sessions"); + const sessionFile = + await findMatchingFile(managedRoot, (_absolutePath, name) => name.includes(input.sessionId!), 6) ?? + await findMatchingFile(sharedRoot, (_absolutePath, name) => name.includes(input.sessionId!), 6); + + const sessionText = await readTextFileIfPresent(sessionFile, input.state, "bundle.rawAdapterTrace.codex.session"); + if (!sessionText) { + appendNote(input.notes, "codex_session_file_missing"); + return { files, raw: null as Record | null, normalized: null as Record | null }; + } + + files.push(makeBundleFile({ + path: "adapter/codex/session.jsonl", + contentType: "application/x-ndjson", + source: "codex_session", + contents: sessionText, + })); + + return { + files, + raw: { + adapterType: "codex_local", + sessionId: input.sessionId, + sessionFile: sessionFile ? path.basename(sessionFile) : null, + }, + normalized: sanitizeFeedbackValue( + { + adapterType: "codex_local", + sessionId: input.sessionId, + summary: parseCodexJsonl(sessionText), + }, + input.state, + "bundle.normalizedAdapterTrace.codex", + MAX_TRACE_FILE_CHARS, + ) as Record, + }; +} + +async function buildClaudeTraceFiles(input: { + sessionId: string | null; + stdoutText: string; + state: ReturnType; + notes: string[]; +}) { + const files: FeedbackTraceBundleFile[] = []; + const sanitizedStdout = sanitizeFeedbackText( + input.stdoutText, + input.state, + "bundle.rawAdapterTrace.claude.stdout", + MAX_TRACE_FILE_CHARS, + ); + if (sanitizedStdout.trim().length > 0) { + files.push(makeBundleFile({ + path: "adapter/claude/stream-json.ndjson", + contentType: "application/x-ndjson", + source: "claude_stream_json", + contents: sanitizedStdout, + })); + } + + const projectsRoot = path.join(claudeConfigDir(), "projects"); + const projectSessionFile = input.sessionId + ? await findMatchingFile(projectsRoot, (_absolutePath, name) => name === `${input.sessionId}.jsonl`, 6) + : null; + const projectSessionText = await readTextFileIfPresent( + projectSessionFile, + input.state, + "bundle.rawAdapterTrace.claude.projectSession", + ); + if (projectSessionText) { + files.push(makeBundleFile({ + path: "adapter/claude/session.jsonl", + contentType: "application/x-ndjson", + source: "claude_project_session", + contents: projectSessionText, + })); + } else if (input.sessionId) { + appendNote(input.notes, "claude_project_session_missing"); + } + + const projectSessionArtifactsDir = projectSessionFile + ? path.join(path.dirname(projectSessionFile), input.sessionId ?? "") + : null; + const projectSessionArtifactFiles = projectSessionArtifactsDir + ? await listNestedFiles(projectSessionArtifactsDir, 4) + : []; + for (const filePath of projectSessionArtifactFiles) { + const relativePath = path.relative(projectSessionArtifactsDir!, filePath).split(path.sep).join("/"); + const fileText = await readTextFileIfPresent( + filePath, + input.state, + `bundle.rawAdapterTrace.claude.projectArtifacts.${relativePath}`, + ); + if (!fileText) continue; + files.push(makeBundleFile({ + path: `adapter/claude/session/${relativePath}`, + contentType: contentTypeForPath(filePath), + source: "claude_project_artifact", + contents: fileText, + })); + } + + const debugLogText = await readTextFileIfPresent( + input.sessionId ? path.join(claudeConfigDir(), "debug", `${input.sessionId}.txt`) : null, + input.state, + "bundle.rawAdapterTrace.claude.debugLog", + ); + if (debugLogText) { + files.push(makeBundleFile({ + path: "adapter/claude/debug.txt", + contentType: "text/plain; charset=utf-8", + source: "claude_debug_log", + contents: debugLogText, + })); + } + + const taskDir = input.sessionId ? path.join(claudeConfigDir(), "tasks", input.sessionId) : null; + const taskFiles = taskDir ? await listChildFiles(taskDir) : []; + const metadataPieces: string[] = []; + for (const filePath of taskFiles) { + const fileText = await readTextFileIfPresent( + filePath, + input.state, + `bundle.rawAdapterTrace.claude.taskMetadata.${path.basename(filePath)}`, + ); + if (!fileText) continue; + metadataPieces.push(`# ${path.basename(filePath)}\n${fileText}`); + } + if (metadataPieces.length > 0) { + files.push(makeBundleFile({ + path: "adapter/claude/task-metadata.txt", + contentType: "text/plain; charset=utf-8", + source: "claude_task_metadata", + contents: `${metadataPieces.join("\n\n")}\n`, + })); + } else if (input.sessionId) { + appendNote(input.notes, "claude_task_metadata_missing"); + } + + if (files.length === 0) { + appendNote(input.notes, "claude_stream_trace_missing"); + } + + return { + files, + raw: { + adapterType: "claude_local", + sessionId: input.sessionId, + projectSessionFound: Boolean(projectSessionText), + projectArtifactsCount: projectSessionArtifactFiles.length, + debugLogFound: Boolean(debugLogText), + taskDirPresent: taskFiles.length > 0, + }, + normalized: sanitizeFeedbackValue( + { + adapterType: "claude_local", + sessionId: input.sessionId, + summary: parseClaudeStreamJson(input.stdoutText), + }, + input.state, + "bundle.normalizedAdapterTrace.claude", + MAX_TRACE_FILE_CHARS, + ) as Record, + }; +} + +async function buildOpenCodeTraceFiles(input: { + sessionId: string | null; + stdoutText: string; + state: ReturnType; + notes: string[]; +}) { + const files: FeedbackTraceBundleFile[] = []; + if (!input.sessionId) { + appendNote(input.notes, "opencode_session_id_missing"); + return { + files, + raw: null as Record | null, + normalized: sanitizeFeedbackValue( + { + adapterType: "opencode_local", + summary: parseOpenCodeJsonl(input.stdoutText), + }, + input.state, + "bundle.normalizedAdapterTrace.opencode", + MAX_TRACE_FILE_CHARS, + ) as Record, + }; + } + + const opencodeRoot = resolveHomeAwarePath( + process.env.PAPERCLIP_OPENCODE_STORAGE_DIR ?? "~/.local/share/opencode", + ); + const sessionRoot = path.join(opencodeRoot, "storage", "session"); + const diffRoot = path.join(opencodeRoot, "storage", "session_diff"); + const messageRoot = path.join(opencodeRoot, "storage", "message"); + const partRoot = path.join(opencodeRoot, "storage", "part"); + const todoRoot = path.join(opencodeRoot, "storage", "todo"); + const projectRoot = path.join(opencodeRoot, "storage", "project"); + const sessionFile = await findMatchingFile( + sessionRoot, + (_absolutePath, name) => name === `${input.sessionId}.json`, + 6, + ); + const diffFile = path.join(diffRoot, `${input.sessionId}.json`); + + const sessionRaw = sessionFile ? await readFile(sessionFile, "utf8").catch(() => null) : null; + const sessionText = + sessionRaw == null + ? null + : sanitizeFeedbackText(sessionRaw, input.state, "bundle.rawAdapterTrace.opencode.session", MAX_TRACE_FILE_CHARS); + if (sessionText) { + files.push(makeBundleFile({ + path: "adapter/opencode/session.json", + contentType: "application/json", + source: "opencode_session", + contents: sessionText, + })); + } else { + appendNote(input.notes, "opencode_session_file_missing"); + } + + const diffText = await readTextFileIfPresent( + diffFile, + input.state, + "bundle.rawAdapterTrace.opencode.sessionDiff", + ); + if (diffText) { + files.push(makeBundleFile({ + path: "adapter/opencode/session-diff.json", + contentType: "application/json", + source: "opencode_session_diff", + contents: diffText, + })); + } + + const messageFiles = await listChildFiles(path.join(messageRoot, input.sessionId)); + const messageIds: string[] = []; + for (const filePath of messageFiles) { + const messageText = await readTextFileIfPresent( + filePath, + input.state, + `bundle.rawAdapterTrace.opencode.messages.${path.basename(filePath)}`, + ); + if (!messageText) continue; + messageIds.push(path.basename(filePath, path.extname(filePath))); + files.push(makeBundleFile({ + path: `adapter/opencode/messages/${path.basename(filePath)}`, + contentType: "application/json", + source: "opencode_message", + contents: messageText, + })); + } + if (messageFiles.length === 0) { + appendNote(input.notes, "opencode_message_files_missing"); + } + + let partFilesCount = 0; + for (const messageId of messageIds) { + const partFiles = await listChildFiles(path.join(partRoot, messageId)); + for (const filePath of partFiles) { + const partText = await readTextFileIfPresent( + filePath, + input.state, + `bundle.rawAdapterTrace.opencode.parts.${messageId}.${path.basename(filePath)}`, + ); + if (!partText) continue; + partFilesCount += 1; + files.push(makeBundleFile({ + path: `adapter/opencode/parts/${messageId}/${path.basename(filePath)}`, + contentType: "application/json", + source: "opencode_message_part", + contents: partText, + })); + } + } + if (messageIds.length > 0 && partFilesCount === 0) { + appendNote(input.notes, "opencode_message_parts_missing"); + } + + const parsedSession = (() => { + if (!sessionRaw) return null; + try { + return JSON.parse(sessionRaw) as Record; + } catch { + return null; + } + })(); + const projectId = asString(parsedSession?.projectID) ?? asString(parsedSession?.projectId); + const projectText = await readTextFileIfPresent( + projectId ? path.join(projectRoot, `${projectId}.json`) : null, + input.state, + "bundle.rawAdapterTrace.opencode.project", + ); + if (projectText) { + files.push(makeBundleFile({ + path: "adapter/opencode/project.json", + contentType: "application/json", + source: "opencode_project", + contents: projectText, + })); + } + + const todoText = await readTextFileIfPresent( + path.join(todoRoot, `${input.sessionId}.json`), + input.state, + "bundle.rawAdapterTrace.opencode.todo", + ); + if (todoText) { + files.push(makeBundleFile({ + path: "adapter/opencode/todo.json", + contentType: "application/json", + source: "opencode_todo", + contents: todoText, + })); + } + + return { + files, + raw: { + adapterType: "opencode_local", + sessionId: input.sessionId, + sessionFileFound: Boolean(sessionText), + sessionDiffFound: Boolean(diffText), + messageFilesCount: messageFiles.length, + partFilesCount, + projectFound: Boolean(projectText), + todoFound: Boolean(todoText), + }, + normalized: sanitizeFeedbackValue( + { + adapterType: "opencode_local", + sessionId: input.sessionId, + summary: parseOpenCodeJsonl(input.stdoutText), + }, + input.state, + "bundle.normalizedAdapterTrace.opencode", + MAX_TRACE_FILE_CHARS, + ) as Record, + }; +} + +function truncateFailureReason(error: unknown) { + const message = error instanceof Error ? error.message : String(error); + return message.trim().slice(0, 1_000) || "Feedback export failed"; +} + +function mapTraceRow(row: FeedbackTraceRow, includePayload: boolean): FeedbackTrace { + const targetSummary = asRecord(row.targetSummary) as unknown as FeedbackTraceTargetSummary | null; + return { + id: row.id, + companyId: row.companyId, + feedbackVoteId: row.feedbackVoteId, + issueId: row.issueId, + projectId: row.projectId ?? null, + issueIdentifier: row.issueIdentifier, + issueTitle: row.issueTitle, + authorUserId: row.authorUserId, + targetType: row.targetType as FeedbackTargetType, + targetId: row.targetId, + vote: row.vote as FeedbackVoteValue, + status: row.status as FeedbackTraceStatus, + destination: row.destination ?? null, + exportId: row.exportId ?? null, + consentVersion: row.consentVersion ?? null, + schemaVersion: row.schemaVersion, + bundleVersion: row.bundleVersion, + payloadVersion: row.payloadVersion, + payloadDigest: row.payloadDigest ?? null, + payloadSnapshot: includePayload ? asRecord(row.payloadSnapshot) : null, + targetSummary: targetSummary ?? buildTargetSummary({ + label: row.targetType, + excerpt: null, + authorAgentId: null, + authorUserId: null, + createdAt: null, + }), + redactionSummary: asRecord(row.redactionSummary), + attemptCount: row.attemptCount, + lastAttemptedAt: row.lastAttemptedAt ?? null, + exportedAt: row.exportedAt ?? null, + failureReason: row.failureReason ?? null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +async function resolveFeedbackTarget( + db: Pick, + issue: IssueFeedbackContext, + targetType: FeedbackTargetType, + targetId: string, +): Promise { + const issuePath = buildIssuePath(issue.identifier); + + if (targetType === "issue_comment") { + const targetComment = await db + .select({ + id: issueComments.id, + issueId: issueComments.issueId, + companyId: issueComments.companyId, + authorAgentId: issueComments.authorAgentId, + authorUserId: issueComments.authorUserId, + createdByRunId: issueComments.createdByRunId, + body: issueComments.body, + createdAt: issueComments.createdAt, + }) + .from(issueComments) + .where(eq(issueComments.id, targetId)) + .then((rows) => rows[0] ?? null); + + if (!targetComment || targetComment.issueId !== issue.id || targetComment.companyId !== issue.companyId) { + throw notFound("Feedback target not found"); + } + if (!targetComment.authorAgentId) { + throw unprocessable("Feedback voting is only available on agent-authored issue comments"); + } + + const record: ResolvedFeedbackTarget = { + targetType, + targetId, + label: "Comment", + body: targetComment.body, + createdAt: targetComment.createdAt, + authorAgentId: targetComment.authorAgentId, + authorUserId: targetComment.authorUserId, + createdByRunId: targetComment.createdByRunId ?? null, + documentId: null, + documentKey: null, + documentTitle: null, + revisionNumber: null, + issuePath, + targetPath: issuePath ? `${issuePath}#comment-${targetComment.id}` : null, + payloadTarget: { + type: targetType, + id: targetComment.id, + createdAt: targetComment.createdAt.toISOString(), + authorAgentId: targetComment.authorAgentId, + authorUserId: targetComment.authorUserId, + createdByRunId: targetComment.createdByRunId ?? null, + issuePath, + targetPath: issuePath ? `${issuePath}#comment-${targetComment.id}` : null, + }, + }; + return record; + } + + if (targetType === "issue_document_revision") { + const targetRevision = await db + .select({ + id: documentRevisions.id, + companyId: documentRevisions.companyId, + documentId: documentRevisions.documentId, + revisionNumber: documentRevisions.revisionNumber, + body: documentRevisions.body, + createdByAgentId: documentRevisions.createdByAgentId, + createdByUserId: documentRevisions.createdByUserId, + createdByRunId: documentRevisions.createdByRunId, + createdAt: documentRevisions.createdAt, + issueId: issueDocuments.issueId, + key: issueDocuments.key, + title: documents.title, + }) + .from(documentRevisions) + .innerJoin(documents, eq(documentRevisions.documentId, documents.id)) + .innerJoin(issueDocuments, eq(issueDocuments.documentId, documents.id)) + .where(eq(documentRevisions.id, targetId)) + .then((rows) => rows.find((row) => row.issueId === issue.id) ?? null); + + if (!targetRevision || targetRevision.companyId !== issue.companyId) { + throw notFound("Feedback target not found"); + } + if (!targetRevision.createdByAgentId) { + throw unprocessable("Feedback voting is only available on agent-authored document revisions"); + } + + const record: ResolvedFeedbackTarget = { + targetType, + targetId, + label: `${targetRevision.key} rev ${targetRevision.revisionNumber}`, + body: targetRevision.body, + createdAt: targetRevision.createdAt, + authorAgentId: targetRevision.createdByAgentId, + authorUserId: targetRevision.createdByUserId, + createdByRunId: targetRevision.createdByRunId ?? null, + documentId: targetRevision.documentId, + documentKey: targetRevision.key, + documentTitle: targetRevision.title ?? null, + revisionNumber: targetRevision.revisionNumber, + issuePath, + targetPath: issuePath ? `${issuePath}#document-${encodeURIComponent(targetRevision.key)}` : null, + payloadTarget: { + type: targetType, + id: targetRevision.id, + documentId: targetRevision.documentId, + documentKey: targetRevision.key, + documentTitle: targetRevision.title ?? null, + revisionNumber: targetRevision.revisionNumber, + createdAt: targetRevision.createdAt.toISOString(), + authorAgentId: targetRevision.createdByAgentId, + authorUserId: targetRevision.createdByUserId, + createdByRunId: targetRevision.createdByRunId ?? null, + issuePath, + targetPath: issuePath ? `${issuePath}#document-${encodeURIComponent(targetRevision.key)}` : null, + }, + }; + return record; + } + + throw unprocessable("Unsupported feedback target type"); +} + +async function listIssueContextItems( + db: Pick, + issue: IssueFeedbackContext, +) { + const [commentRows, revisionRows] = await Promise.all([ + db + .select({ + targetId: issueComments.id, + body: issueComments.body, + createdAt: issueComments.createdAt, + authorAgentId: issueComments.authorAgentId, + authorUserId: issueComments.authorUserId, + createdByRunId: issueComments.createdByRunId, + }) + .from(issueComments) + .where(and(eq(issueComments.companyId, issue.companyId), eq(issueComments.issueId, issue.id))), + db + .select({ + targetId: documentRevisions.id, + body: documentRevisions.body, + createdAt: documentRevisions.createdAt, + authorAgentId: documentRevisions.createdByAgentId, + authorUserId: documentRevisions.createdByUserId, + createdByRunId: documentRevisions.createdByRunId, + documentId: documentRevisions.documentId, + documentKey: issueDocuments.key, + documentTitle: documents.title, + revisionNumber: documentRevisions.revisionNumber, + }) + .from(documentRevisions) + .innerJoin(documents, eq(documentRevisions.documentId, documents.id)) + .innerJoin(issueDocuments, eq(issueDocuments.documentId, documents.id)) + .where(and(eq(documentRevisions.companyId, issue.companyId), eq(issueDocuments.issueId, issue.id))), + ]); + + const issuePath = buildIssuePath(issue.identifier); + + const items: FeedbackTargetRecord[] = [ + ...commentRows.map((row) => ({ + targetType: "issue_comment" as const, + targetId: row.targetId, + label: "Comment", + body: row.body, + createdAt: row.createdAt, + authorAgentId: row.authorAgentId, + authorUserId: row.authorUserId, + createdByRunId: row.createdByRunId ?? null, + documentId: null, + documentKey: null, + documentTitle: null, + revisionNumber: null, + issuePath, + targetPath: issuePath ? `${issuePath}#comment-${row.targetId}` : null, + })), + ...revisionRows.map((row) => ({ + targetType: "issue_document_revision" as const, + targetId: row.targetId, + label: `${row.documentKey} rev ${row.revisionNumber}`, + body: row.body, + createdAt: row.createdAt, + authorAgentId: row.authorAgentId, + authorUserId: row.authorUserId, + createdByRunId: row.createdByRunId ?? null, + documentId: row.documentId, + documentKey: row.documentKey, + documentTitle: row.documentTitle ?? null, + revisionNumber: row.revisionNumber, + issuePath, + targetPath: issuePath ? `${issuePath}#document-${encodeURIComponent(row.documentKey)}` : null, + })), + ]; + + return items.sort((left, right) => { + const byDate = left.createdAt.getTime() - right.createdAt.getTime(); + if (byDate !== 0) return byDate; + return left.targetId.localeCompare(right.targetId); + }); +} + +async function buildIssueContext( + db: Pick, + issue: IssueFeedbackContext, + target: ResolvedFeedbackTarget, + state: ReturnType, +) { + const items = await listIssueContextItems(db, issue); + const targetIndex = items.findIndex((item) => item.targetType === target.targetType && item.targetId === target.targetId); + const before = targetIndex >= 0 + ? items.slice(Math.max(0, targetIndex - FEEDBACK_CONTEXT_WINDOW), targetIndex) + : []; + const after = targetIndex >= 0 + ? items.slice(targetIndex + 1, targetIndex + 1 + FEEDBACK_CONTEXT_WINDOW) + : []; + + let remainingChars = MAX_TOTAL_CONTEXT_CHARS; + const serializedItems = [...before, ...after].map((item, index) => { + const relation = index < before.length ? "before" : "after"; + if (remainingChars <= 0) { + state.omittedFields.add("bundle.issueContext.items"); + return null; + } + const maxChars = Math.min(MAX_CONTEXT_ITEM_BODY_CHARS, remainingChars); + const body = sanitizeFeedbackText( + item.body, + state, + `bundle.issueContext.items.${index}.body`, + maxChars, + ); + remainingChars -= body.length; + return { + type: item.targetType, + id: item.targetId, + label: item.label, + relation, + createdAt: item.createdAt.toISOString(), + authorAgentId: item.authorAgentId, + authorUserId: item.authorUserId, + createdByRunId: item.createdByRunId, + documentKey: item.documentKey, + documentTitle: item.documentTitle, + revisionNumber: item.revisionNumber, + targetPath: item.targetPath, + body, + excerpt: truncateExcerpt(body), + }; + }).filter((item): item is NonNullable => item !== null); + + const descriptionExcerpt = issue.description + ? sanitizeFeedbackText(issue.description, state, "bundle.issueContext.issue.description", MAX_DESCRIPTION_CHARS) + : null; + + return { + issue: { + id: issue.id, + identifier: issue.identifier, + title: issue.title, + projectId: issue.projectId, + path: buildIssuePath(issue.identifier), + descriptionExcerpt: descriptionExcerpt ? truncateExcerpt(descriptionExcerpt, MAX_DESCRIPTION_CHARS) : null, + }, + items: serializedItems, + }; +} + +async function buildAgentContext( + db: Pick, + companyId: string, + authorAgentId: string | null, + createdByRunId: string | null, + state: ReturnType, +) { + if (!authorAgentId) { + state.notes.add("author_agent_missing"); + return null; + } + + const agent = await db + .select({ + id: agents.id, + companyId: agents.companyId, + name: agents.name, + role: agents.role, + title: agents.title, + status: agents.status, + adapterType: agents.adapterType, + adapterConfig: agents.adapterConfig, + runtimeConfig: agents.runtimeConfig, + }) + .from(agents) + .where(eq(agents.id, authorAgentId)) + .then((rows) => rows[0] ?? null); + + if (!agent || agent.companyId !== companyId) { + state.notes.add("author_agent_unavailable"); + return null; + } + + const adapterConfig = asRecord(agent.adapterConfig) ?? {}; + const runtimeConfig = asRecord(agent.runtimeConfig) ?? {}; + const desiredSkillRefs = uniqueNonEmpty(readPaperclipSkillSyncPreference(adapterConfig).desiredSkills).slice(0, MAX_SKILLS); + const availableSkills = desiredSkillRefs.length === 0 + ? [] + : await db + .select() + .from(companySkills) + .where(eq(companySkills.companyId, companyId)); + const matchedSkills = availableSkills + .filter((skill) => desiredSkillRefs.some((reference) => matchesSkillReference(skill, reference))) + .slice(0, MAX_SKILLS); + const unresolvedSkillRefs = desiredSkillRefs.filter( + (reference) => !matchedSkills.some((skill) => matchesSkillReference(skill, reference)), + ); + + if (availableSkills.length > MAX_SKILLS || desiredSkillRefs.length > MAX_SKILLS) { + state.omittedFields.add("bundle.agentContext.skills"); + } + + const run = createdByRunId + ? await db + .select({ + id: heartbeatRuns.id, + companyId: heartbeatRuns.companyId, + agentId: heartbeatRuns.agentId, + invocationSource: heartbeatRuns.invocationSource, + status: heartbeatRuns.status, + startedAt: heartbeatRuns.startedAt, + finishedAt: heartbeatRuns.finishedAt, + usageJson: heartbeatRuns.usageJson, + sessionIdBefore: heartbeatRuns.sessionIdBefore, + sessionIdAfter: heartbeatRuns.sessionIdAfter, + externalRunId: heartbeatRuns.externalRunId, + }) + .from(heartbeatRuns) + .where(eq(heartbeatRuns.id, createdByRunId)) + .then((rows) => rows[0] ?? null) + : null; + const runCosts = run + ? await db + .select({ + provider: costEvents.provider, + biller: costEvents.biller, + billingType: costEvents.billingType, + model: costEvents.model, + inputTokens: costEvents.inputTokens, + cachedInputTokens: costEvents.cachedInputTokens, + outputTokens: costEvents.outputTokens, + costCents: costEvents.costCents, + }) + .from(costEvents) + .where(and(eq(costEvents.companyId, companyId), eq(costEvents.heartbeatRunId, run.id))) + : []; + + const usage = asRecord(run?.usageJson) ?? {}; + const runtime = { + configuredModel: asString(adapterConfig.model), + configuredInstructionsBundleMode: asString(adapterConfig.instructionsBundleMode), + configuredInstructionsEntryFile: asString(adapterConfig.instructionsEntryFile), + configuredInstructionsFilePath: asString(adapterConfig.instructionsFilePath), + configuredInstructionsRootPath: asString(adapterConfig.instructionsRootPath), + heartbeatPolicy: sanitizeFeedbackValue(runtimeConfig.heartbeat ?? null, state, "bundle.agentContext.runtime.heartbeatPolicy", 400), + provenanceMode: run ? "source_run" : "vote_time_snapshot", + sourceRun: run + ? sanitizeFeedbackValue({ + id: run.id, + invocationSource: run.invocationSource, + status: run.status, + startedAt: run.startedAt?.toISOString() ?? null, + finishedAt: run.finishedAt?.toISOString() ?? null, + externalRunId: run.externalRunId ?? null, + sessionIdBefore: run.sessionIdBefore ?? null, + sessionIdAfter: run.sessionIdAfter ?? null, + usage: { + provider: asString(usage.provider), + biller: asString(usage.biller), + billingType: asString(usage.billingType), + model: asString(usage.model), + inputTokens: asNumber(usage.inputTokens) ?? asNumber(usage.rawInputTokens), + cachedInputTokens: asNumber(usage.cachedInputTokens) ?? asNumber(usage.rawCachedInputTokens), + outputTokens: asNumber(usage.outputTokens) ?? asNumber(usage.rawOutputTokens), + costUsd: asNumber(usage.costUsd), + usageSource: asString(usage.usageSource), + sessionReused: asBoolean(usage.sessionReused), + taskSessionReused: asBoolean(usage.taskSessionReused), + freshSession: asBoolean(usage.freshSession), + sessionRotated: asBoolean(usage.sessionRotated), + sessionRotationReason: asString(usage.sessionRotationReason), + }, + }, state, "bundle.agentContext.runtime.sourceRun", 400) + : null, + costSummary: runCosts.length > 0 + ? { + providers: uniqueNonEmpty(runCosts.map((row) => row.provider)), + billers: uniqueNonEmpty(runCosts.map((row) => row.biller)), + billingTypes: uniqueNonEmpty(runCosts.map((row) => row.billingType)), + models: uniqueNonEmpty(runCosts.map((row) => row.model)), + inputTokens: runCosts.reduce((sum, row) => sum + row.inputTokens, 0), + cachedInputTokens: runCosts.reduce((sum, row) => sum + row.cachedInputTokens, 0), + outputTokens: runCosts.reduce((sum, row) => sum + row.outputTokens, 0), + costCents: runCosts.reduce((sum, row) => sum + row.costCents, 0), + } + : null, + }; + + const instructionsBundle = await instructionsSvc.getBundle({ + id: agent.id, + companyId: agent.companyId, + name: agent.name, + adapterConfig: agent.adapterConfig, + }).catch(() => null); + + let entryDigest: string | null = null; + let entryBody: string | null = null; + if (instructionsBundle) { + const readableEntryPath = + instructionsBundle.files.find((file) => file.path === instructionsBundle.entryFile)?.path + ?? instructionsBundle.files[0]?.path + ?? null; + if (readableEntryPath) { + const entryFile = await instructionsSvc.readFile({ + id: agent.id, + companyId: agent.companyId, + name: agent.name, + adapterConfig: agent.adapterConfig, + }, readableEntryPath).catch(() => null); + if (entryFile) { + entryDigest = sha256Digest(entryFile.content); + entryBody = sanitizeFeedbackText( + entryFile.content, + state, + "bundle.agentContext.instructions.entryBody", + MAX_INSTRUCTIONS_BODY_CHARS, + ); + } + } + if (instructionsBundle.files.length > MAX_INSTRUCTION_FILES) { + state.omittedFields.add("bundle.agentContext.instructions.files"); + } + } + + return { + agent: { + id: agent.id, + name: agent.name, + role: agent.role, + title: agent.title, + status: agent.status, + adapterType: agent.adapterType, + }, + runtime: sanitizeFeedbackValue(runtime, state, "bundle.agentContext.runtime", 400), + skills: { + desiredRefs: desiredSkillRefs, + unresolvedRefs: unresolvedSkillRefs, + items: matchedSkills.map((skill, index) => ({ + key: skill.key, + slug: skill.slug, + name: skill.name, + sourceType: skill.sourceType, + sourceLocator: skill.sourceLocator == null + ? null + : skill.sourceType === "github" || skill.sourceType === "skills_sh" || skill.sourceType === "url" + ? skill.sourceLocator + : sanitizeFeedbackText( + skill.sourceLocator, + state, + `bundle.agentContext.skills.items.${index}.sourceLocator`, + MAX_PATH_CHARS, + ), + sourceRef: skill.sourceRef, + trustLevel: skill.trustLevel, + compatibility: skill.compatibility, + fileInventory: skill.fileInventory, + })), + }, + instructions: instructionsBundle + ? { + mode: instructionsBundle.mode, + entryFile: instructionsBundle.entryFile, + resolvedEntryPath: instructionsBundle.resolvedEntryPath + ? sanitizeFeedbackText( + instructionsBundle.resolvedEntryPath, + state, + "bundle.agentContext.instructions.resolvedEntryPath", + MAX_PATH_CHARS, + ) + : null, + warnings: instructionsBundle.warnings.map((warning, index) => + sanitizeFeedbackText( + warning, + state, + `bundle.agentContext.instructions.warnings.${index}`, + 400, + )), + legacyPromptTemplateActive: instructionsBundle.legacyPromptTemplateActive, + legacyBootstrapPromptTemplateActive: instructionsBundle.legacyBootstrapPromptTemplateActive, + fileCount: instructionsBundle.files.length, + files: instructionsBundle.files.slice(0, MAX_INSTRUCTION_FILES).map((file) => ({ + path: file.path, + size: file.size, + language: file.language, + markdown: file.markdown, + isEntryFile: file.isEntryFile, + virtual: file.virtual, + })), + entryDigest, + entryBody, + } + : null, + paperclip: { + schemaVersion: FEEDBACK_SCHEMA_VERSION, + bundleVersion: FEEDBACK_BUNDLE_VERSION, + }, + }; +} + +async function buildPayloadArtifacts( + db: Pick, + input: { + issue: IssueFeedbackContext; + target: ResolvedFeedbackTarget; + voteId: string; + vote: FeedbackVoteValue; + reason: string | null; + authorUserId: string; + consentVersion: string | null; + sharedWithLabs: boolean; + now: Date; + }, +) { + const state = createFeedbackRedactionState(); + const primaryBody = sanitizeFeedbackText( + input.target.body, + state, + "bundle.primaryContent.body", + MAX_PRIMARY_CONTENT_CHARS, + ); + const primaryContent = { + type: input.target.targetType, + id: input.target.targetId, + label: input.target.label, + createdAt: input.target.createdAt.toISOString(), + authorAgentId: input.target.authorAgentId, + authorUserId: input.target.authorUserId, + createdByRunId: input.target.createdByRunId, + documentId: input.target.documentId, + documentKey: input.target.documentKey, + documentTitle: input.target.documentTitle, + revisionNumber: input.target.revisionNumber, + targetPath: input.target.targetPath, + body: primaryBody, + excerpt: truncateExcerpt(primaryBody), + }; + const targetSummary = buildTargetSummary({ + label: input.target.label, + excerpt: primaryContent.excerpt, + authorAgentId: input.target.authorAgentId, + authorUserId: input.target.authorUserId, + createdAt: input.target.createdAt, + documentKey: input.target.documentKey, + documentTitle: input.target.documentTitle, + revisionNumber: input.target.revisionNumber, + }); + + const basePayload = { + schemaVersion: FEEDBACK_SCHEMA_VERSION, + bundleVersion: FEEDBACK_BUNDLE_VERSION, + sourceApp: "paperclip", + capturedAt: input.now.toISOString(), + consentVersion: input.consentVersion, + vote: { + id: input.voteId, + value: input.vote, + reason: input.reason, + authorUserId: input.authorUserId, + sharedWithLabs: input.sharedWithLabs, + sharedAt: input.sharedWithLabs ? input.now.toISOString() : null, + }, + target: input.target.payloadTarget, + } satisfies Record; + + if (!input.sharedWithLabs) { + state.notes.add("local_only_trace_stores_metadata_only"); + const payloadSnapshot = { + ...basePayload, + exportId: null, + exportEligible: false, + bundle: null, + }; + const redactionSummary = finalizeFeedbackRedactionSummary(state); + return { + exportId: null, + targetSummary, + redactionSummary, + payloadSnapshot: { + ...payloadSnapshot, + redactionSummary, + }, + payloadDigest: sha256Digest({ + ...payloadSnapshot, + redactionSummary, + }), + }; + } + + const exportId = buildExportId(input.voteId, input.now); + const [issueContext, agentContext] = await Promise.all([ + buildIssueContext(db, input.issue, input.target, state), + buildAgentContext(db, input.issue.companyId, input.target.authorAgentId, input.target.createdByRunId, state), + ]); + + const payloadSnapshot = { + ...basePayload, + exportId, + exportEligible: true, + bundle: { + primaryContent, + issueContext, + agentContext, + }, + }; + const redactionSummary = finalizeFeedbackRedactionSummary(state); + const payloadWithSummary = { + ...payloadSnapshot, + redactionSummary, + }; + return { + exportId, + targetSummary, + redactionSummary, + payloadSnapshot: payloadWithSummary, + payloadDigest: sha256Digest(payloadWithSummary), + }; +} + +async function buildFeedbackTraceBundleFromRow( + db: Db, + row: FeedbackTraceRow, +): Promise { + const trace = mapTraceRow(row, true); + const payloadSnapshot = asRecord(trace.payloadSnapshot); + const notes: string[] = []; + const state = createFeedbackRedactionState(); + const files: FeedbackTraceBundleFile[] = []; + const sourceRunId = resolveSourceRunId(payloadSnapshot); + + let paperclipRun: Record | null = null; + let rawAdapterTrace: Record | null = null; + let normalizedAdapterTrace: Record | null = null; + let adapterType: string | null = null; + + if (!sourceRunId) { + appendNote(notes, "source_run_missing"); + } else { + const run = await db + .select({ + id: heartbeatRuns.id, + companyId: heartbeatRuns.companyId, + agentId: heartbeatRuns.agentId, + invocationSource: heartbeatRuns.invocationSource, + status: heartbeatRuns.status, + startedAt: heartbeatRuns.startedAt, + finishedAt: heartbeatRuns.finishedAt, + createdAt: heartbeatRuns.createdAt, + updatedAt: heartbeatRuns.updatedAt, + error: heartbeatRuns.error, + errorCode: heartbeatRuns.errorCode, + usageJson: heartbeatRuns.usageJson, + resultJson: heartbeatRuns.resultJson, + sessionIdBefore: heartbeatRuns.sessionIdBefore, + sessionIdAfter: heartbeatRuns.sessionIdAfter, + externalRunId: heartbeatRuns.externalRunId, + contextSnapshot: heartbeatRuns.contextSnapshot, + logStore: heartbeatRuns.logStore, + logRef: heartbeatRuns.logRef, + logBytes: heartbeatRuns.logBytes, + logSha256: heartbeatRuns.logSha256, + agentName: agents.name, + agentRole: agents.role, + agentTitle: agents.title, + adapterType: agents.adapterType, + }) + .from(heartbeatRuns) + .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id)) + .where(eq(heartbeatRuns.id, sourceRunId)) + .then((rows) => rows[0] ?? null); + + if (!run || run.companyId !== row.companyId) { + appendNote(notes, "source_run_unavailable"); + } else { + adapterType = run.adapterType; + const events = await db + .select() + .from(heartbeatRunEvents) + .where(eq(heartbeatRunEvents.runId, run.id)) + .orderBy(asc(heartbeatRunEvents.seq)); + const logText = await readFullRunLog(run); + const logEntries = parseRunLogEntries(logText); + const stdoutText = logEntries + .filter((entry) => entry.stream === "stdout") + .map((entry) => entry.chunk) + .join(""); + + paperclipRun = sanitizeFeedbackValue( + { + id: run.id, + companyId: run.companyId, + agentId: run.agentId, + agentName: run.agentName, + agentRole: run.agentRole, + agentTitle: run.agentTitle, + adapterType: run.adapterType, + invocationSource: run.invocationSource, + status: run.status, + startedAt: run.startedAt?.toISOString() ?? null, + finishedAt: run.finishedAt?.toISOString() ?? null, + createdAt: run.createdAt.toISOString(), + updatedAt: run.updatedAt.toISOString(), + error: run.error, + errorCode: run.errorCode, + usage: asRecord(run.usageJson), + result: asRecord(run.resultJson), + sessionIdBefore: run.sessionIdBefore, + sessionIdAfter: run.sessionIdAfter, + externalRunId: run.externalRunId, + contextSnapshot: asRecord(run.contextSnapshot), + logStore: run.logStore, + logRef: run.logRef, + logBytes: run.logBytes, + logSha256: run.logSha256, + eventCount: events.length, + }, + state, + "bundle.paperclipRun", + MAX_TRACE_FILE_CHARS, + ) as Record; + + files.push(makeBundleFile({ + path: "paperclip/run.json", + contentType: "application/json", + source: "paperclip_run", + contents: `${JSON.stringify(paperclipRun, null, 2)}\n`, + })); + + const sanitizedEvents = sanitizeFeedbackValue( + events, + state, + "bundle.paperclipRun.events", + MAX_TRACE_FILE_CHARS, + ); + files.push(makeBundleFile({ + path: "paperclip/run-events.json", + contentType: "application/json", + source: "paperclip_run_events", + contents: `${JSON.stringify(sanitizedEvents, null, 2)}\n`, + })); + + if (logText) { + files.push(makeBundleFile({ + path: "paperclip/run-log.ndjson", + contentType: "application/x-ndjson", + source: "paperclip_run_log", + contents: `${sanitizeFeedbackText(logText, state, "bundle.paperclipRun.log", MAX_TRACE_FILE_CHARS)}\n`, + })); + } else { + appendNote(notes, "run_log_missing"); + } + + if (run.adapterType === "codex_local") { + const adapter = await buildCodexTraceFiles({ + companyId: row.companyId, + sessionId: run.sessionIdAfter ?? run.sessionIdBefore, + state, + notes, + }); + files.push(...adapter.files); + rawAdapterTrace = adapter.raw; + normalizedAdapterTrace = adapter.normalized; + } else if (run.adapterType === "claude_local") { + const adapter = await buildClaudeTraceFiles({ + sessionId: run.sessionIdAfter ?? run.sessionIdBefore, + stdoutText, + state, + notes, + }); + files.push(...adapter.files); + rawAdapterTrace = adapter.raw; + normalizedAdapterTrace = adapter.normalized; + } else if (run.adapterType === "opencode_local") { + const adapter = await buildOpenCodeTraceFiles({ + sessionId: run.sessionIdAfter ?? run.sessionIdBefore, + stdoutText, + state, + notes, + }); + files.push(...adapter.files); + rawAdapterTrace = adapter.raw; + normalizedAdapterTrace = adapter.normalized; + } else { + appendNote(notes, "adapter_specific_trace_not_supported"); + } + } + } + + const privacy = { + ...(asRecord(trace.redactionSummary) ?? {}), + bundleRedactionSummary: finalizeFeedbackRedactionSummary(state), + }; + const captureStatus = captureStatusFromFiles(files); + if (captureStatus !== "full" && files.length > 0) { + appendNote(notes, "adapter_trace_partial"); + } + + const envelope = sanitizeFeedbackValue( + { + traceId: trace.id, + exportId: trace.exportId, + companyId: trace.companyId, + feedbackVoteId: trace.feedbackVoteId, + issueId: trace.issueId, + issueIdentifier: trace.issueIdentifier, + issueTitle: trace.issueTitle, + projectId: trace.projectId, + authorUserId: trace.authorUserId, + targetType: trace.targetType, + targetId: trace.targetId, + vote: trace.vote, + status: trace.status, + destination: trace.destination, + consentVersion: trace.consentVersion, + schemaVersion: trace.schemaVersion, + bundleVersion: trace.bundleVersion, + payloadVersion: trace.payloadVersion, + payloadDigest: trace.payloadDigest, + createdAt: trace.createdAt.toISOString(), + exportedAt: trace.exportedAt?.toISOString() ?? null, + }, + state, + "bundle.envelope", + MAX_TRACE_FILE_CHARS, + ) as Record; + + const surface = sanitizeFeedbackValue( + { + target: asRecord(payloadSnapshot?.target), + summary: trace.targetSummary, + }, + state, + "bundle.surface", + MAX_TRACE_FILE_CHARS, + ) as Record; + + const bundle: FeedbackTraceBundle = { + traceId: trace.id, + exportId: trace.exportId, + companyId: trace.companyId, + issueId: trace.issueId, + issueIdentifier: trace.issueIdentifier, + adapterType, + captureStatus, + notes, + envelope, + surface, + paperclipRun, + rawAdapterTrace, + normalizedAdapterTrace, + privacy, + integrity: { + payloadDigest: trace.payloadDigest, + bundleDigest: sha256Digest({ + traceId: trace.id, + files: files.map((file) => ({ + path: file.path, + source: file.source, + sha256: file.sha256, + })), + captureStatus, + }), + }, + files, + }; + + return bundle; +} + +export function feedbackService(db: Db, options: FeedbackServiceOptions = {}) { + return { + listIssueVotesForUser: async (issueId: string, authorUserId: string) => + db + .select() + .from(feedbackVotes) + .where(and(eq(feedbackVotes.issueId, issueId), eq(feedbackVotes.authorUserId, authorUserId))), + + listFeedbackTraces: async (input: { + companyId: string; + issueId?: string; + projectId?: string; + targetType?: FeedbackTargetType; + vote?: FeedbackVoteValue; + status?: FeedbackTraceStatus; + from?: Date; + to?: Date; + sharedOnly?: boolean; + includePayload?: boolean; + }) => { + const filters = [eq(feedbackExports.companyId, input.companyId)]; + if (input.issueId) filters.push(eq(feedbackExports.issueId, input.issueId)); + if (input.projectId) filters.push(eq(feedbackExports.projectId, input.projectId)); + if (input.targetType) filters.push(eq(feedbackExports.targetType, input.targetType)); + if (input.vote) filters.push(eq(feedbackExports.vote, input.vote)); + if (input.status) filters.push(eq(feedbackExports.status, input.status)); + if (input.sharedOnly) filters.push(ne(feedbackExports.status, "local_only")); + if (input.from) filters.push(gte(feedbackExports.createdAt, input.from)); + if (input.to) filters.push(lte(feedbackExports.createdAt, input.to)); + + const rows = await db + .select({ + ...feedbackExportColumns, + issueIdentifier: issues.identifier, + issueTitle: issues.title, + }) + .from(feedbackExports) + .innerJoin(issues, eq(feedbackExports.issueId, issues.id)) + .where(and(...filters)) + .orderBy(desc(feedbackExports.createdAt)); + + return rows.map((row) => mapTraceRow(row, input.includePayload === true)); + }, + + getFeedbackTraceById: async (traceId: string, includePayload = true) => { + const row = await db + .select({ + ...feedbackExportColumns, + issueIdentifier: issues.identifier, + issueTitle: issues.title, + }) + .from(feedbackExports) + .innerJoin(issues, eq(feedbackExports.issueId, issues.id)) + .where(eq(feedbackExports.id, traceId)) + .then((rows) => rows[0] ?? null); + return row ? mapTraceRow(row, includePayload) : null; + }, + + getFeedbackTraceBundle: async (traceId: string) => { + const row = await db + .select({ + ...feedbackExportColumns, + issueIdentifier: issues.identifier, + issueTitle: issues.title, + }) + .from(feedbackExports) + .innerJoin(issues, eq(feedbackExports.issueId, issues.id)) + .where(eq(feedbackExports.id, traceId)) + .then((rows) => rows[0] ?? null); + return row ? buildFeedbackTraceBundleFromRow(db, row) : null; + }, + + flushPendingFeedbackTraces: async (input?: { + companyId?: string; + limit?: number; + now?: Date; + }) => { + const shareClient = options.shareClient; + if (!shareClient) { + return { + attempted: 0, + sent: 0, + failed: 0, + }; + } + + const limit = Math.max(1, Math.min(input?.limit ?? 25, 200)); + const filters = [ + or(eq(feedbackExports.status, "pending"), eq(feedbackExports.status, "failed")), + ]; + if (input?.companyId) { + filters.push(eq(feedbackExports.companyId, input.companyId)); + } + + const rows = await db + .select({ + ...feedbackExportColumns, + issueIdentifier: issues.identifier, + issueTitle: issues.title, + }) + .from(feedbackExports) + .innerJoin(issues, eq(feedbackExports.issueId, issues.id)) + .where(and(...filters)) + .orderBy(asc(feedbackExports.createdAt), asc(feedbackExports.id)) + .limit(limit); + + let attempted = 0; + let sent = 0; + let failed = 0; + + for (const row of rows) { + const attemptAt = input?.now ?? new Date(); + attempted += 1; + + try { + const bundle = await buildFeedbackTraceBundleFromRow(db, row); + await shareClient.uploadTraceBundle(bundle); + + await db + .update(feedbackExports) + .set({ + status: "sent", + attemptCount: row.attemptCount + 1, + lastAttemptedAt: attemptAt, + exportedAt: attemptAt, + failureReason: null, + updatedAt: attemptAt, + }) + .where(eq(feedbackExports.id, row.id)); + sent += 1; + } catch (error) { + await db + .update(feedbackExports) + .set({ + status: "failed", + attemptCount: row.attemptCount + 1, + lastAttemptedAt: attemptAt, + failureReason: truncateFailureReason(error), + updatedAt: attemptAt, + }) + .where(eq(feedbackExports.id, row.id)); + failed += 1; + } + } + + return { + attempted, + sent, + failed, + }; + }, + + saveIssueVote: async (input: { + issueId: string; + targetType: FeedbackTargetType; + targetId: string; + vote: FeedbackVoteValue; + authorUserId: string; + reason?: string | null; + allowSharing?: boolean; + }) => + db.transaction(async (tx) => { + const issue = await tx + .select({ + id: issues.id, + companyId: issues.companyId, + projectId: issues.projectId, + identifier: issues.identifier, + title: issues.title, + description: issues.description, + }) + .from(issues) + .where(eq(issues.id, input.issueId)) + .then((rows) => rows[0] ?? null); + if (!issue) throw notFound("Issue not found"); + + const target = await resolveFeedbackTarget(tx, issue, input.targetType, input.targetId); + + const existingCompany = await tx + .select({ + feedbackDataSharingEnabled: companies.feedbackDataSharingEnabled, + feedbackDataSharingTermsVersion: companies.feedbackDataSharingTermsVersion, + }) + .from(companies) + .where(eq(companies.id, issue.companyId)) + .then((rows) => rows[0] ?? null); + if (!existingCompany) throw notFound("Company not found"); + + const now = new Date(); + const normalizedReason = normalizeReason(input.vote, input.reason); + const sharedWithLabs = input.allowSharing === true; + let consentEnabledNow = false; + let consentVersion = existingCompany.feedbackDataSharingTermsVersion ?? null; + let persistedSharingPreference: "allowed" | "not_allowed" | null = null; + + if (sharedWithLabs && !existingCompany.feedbackDataSharingEnabled) { + consentEnabledNow = true; + consentVersion = DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION; + await tx + .update(companies) + .set({ + feedbackDataSharingEnabled: true, + feedbackDataSharingConsentAt: now, + feedbackDataSharingConsentByUserId: input.authorUserId, + feedbackDataSharingTermsVersion: consentVersion, + updatedAt: now, + }) + .where(eq(companies.id, issue.companyId)); + } + + const existingInstanceSettings = await tx + .select({ + id: instanceSettings.id, + general: instanceSettings.general, + }) + .from(instanceSettings) + .where(eq(instanceSettings.singletonKey, DEFAULT_INSTANCE_SETTINGS_SINGLETON_KEY)) + .then((rows) => rows[0] ?? null); + + const currentInstanceSettings = + existingInstanceSettings ?? + (await tx + .insert(instanceSettings) + .values({ + singletonKey: DEFAULT_INSTANCE_SETTINGS_SINGLETON_KEY, + general: {}, + experimental: {}, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [instanceSettings.singletonKey], + set: { + updatedAt: now, + }, + }) + .returning({ + id: instanceSettings.id, + general: instanceSettings.general, + }) + .then((rows) => rows[0] ?? null)); + + const currentGeneral = normalizeInstanceGeneralSettings(currentInstanceSettings?.general); + if (currentInstanceSettings && currentGeneral.feedbackDataSharingPreference === "prompt") { + const nextSharingPreference = sharedWithLabs ? "allowed" : "not_allowed"; + const currentGeneralRaw = asRecord(currentInstanceSettings.general) ?? {}; + await tx + .update(instanceSettings) + .set({ + general: { + ...currentGeneralRaw, + censorUsernameInLogs: currentGeneral.censorUsernameInLogs, + feedbackDataSharingPreference: nextSharingPreference, + }, + updatedAt: now, + }) + .where(eq(instanceSettings.id, currentInstanceSettings.id)); + persistedSharingPreference = nextSharingPreference; + } + + const [savedVote] = await tx + .insert(feedbackVotes) + .values({ + companyId: issue.companyId, + issueId: issue.id, + targetType: input.targetType, + targetId: input.targetId, + authorUserId: input.authorUserId, + vote: input.vote, + reason: normalizedReason, + sharedWithLabs, + sharedAt: sharedWithLabs ? now : null, + consentVersion: sharedWithLabs ? (consentVersion ?? DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION) : null, + redactionSummary: null, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [ + feedbackVotes.companyId, + feedbackVotes.targetType, + feedbackVotes.targetId, + feedbackVotes.authorUserId, + ], + set: { + vote: input.vote, + reason: normalizedReason, + sharedWithLabs, + sharedAt: sharedWithLabs ? now : null, + consentVersion: sharedWithLabs ? (consentVersion ?? DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION) : null, + redactionSummary: null, + updatedAt: now, + }, + }) + .returning(); + + const artifacts = await buildPayloadArtifacts(tx, { + issue, + target, + voteId: savedVote.id, + vote: input.vote, + reason: normalizedReason, + authorUserId: input.authorUserId, + consentVersion: sharedWithLabs ? (consentVersion ?? DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION) : null, + sharedWithLabs, + now, + }); + + await tx + .update(feedbackVotes) + .set({ + redactionSummary: artifacts.redactionSummary, + updatedAt: now, + }) + .where(eq(feedbackVotes.id, savedVote.id)); + + await tx + .insert(feedbackExports) + .values({ + companyId: issue.companyId, + feedbackVoteId: savedVote.id, + issueId: issue.id, + projectId: issue.projectId, + authorUserId: input.authorUserId, + targetType: input.targetType, + targetId: input.targetId, + vote: input.vote, + status: sharedWithLabs ? "pending" : "local_only", + destination: sharedWithLabs ? FEEDBACK_DESTINATION : null, + exportId: artifacts.exportId, + consentVersion: sharedWithLabs ? (consentVersion ?? DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION) : null, + schemaVersion: FEEDBACK_SCHEMA_VERSION, + bundleVersion: FEEDBACK_BUNDLE_VERSION, + payloadVersion: FEEDBACK_BUNDLE_VERSION, + payloadDigest: artifacts.payloadDigest, + payloadSnapshot: artifacts.payloadSnapshot, + targetSummary: artifacts.targetSummary, + redactionSummary: artifacts.redactionSummary, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [feedbackExports.feedbackVoteId], + set: { + issueId: issue.id, + projectId: issue.projectId, + authorUserId: input.authorUserId, + targetType: input.targetType, + targetId: input.targetId, + vote: input.vote, + status: sharedWithLabs ? "pending" : "local_only", + destination: sharedWithLabs ? FEEDBACK_DESTINATION : null, + exportId: artifacts.exportId, + consentVersion: sharedWithLabs ? (consentVersion ?? DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION) : null, + schemaVersion: FEEDBACK_SCHEMA_VERSION, + bundleVersion: FEEDBACK_BUNDLE_VERSION, + payloadVersion: FEEDBACK_BUNDLE_VERSION, + payloadDigest: artifacts.payloadDigest, + payloadSnapshot: artifacts.payloadSnapshot, + targetSummary: artifacts.targetSummary, + redactionSummary: artifacts.redactionSummary, + failureReason: null, + updatedAt: now, + }, + }); + + return { + vote: { + ...savedVote, + redactionSummary: artifacts.redactionSummary, + }, + consentEnabledNow, + persistedSharingPreference, + sharingEnabled: sharedWithLabs, + }; + }), + }; +} diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 18093598..ab6c94da 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -2615,7 +2615,7 @@ export function heartbeatService(db: Db) { workspace: executionWorkspace, runtimeServices, }), - { agentId: agent.id }, + { agentId: agent.id, runId: run.id }, ); } catch (err) { await onLog( @@ -2705,7 +2705,7 @@ export function heartbeatService(db: Db) { workspace: executionWorkspace, runtimeServices: adapterManagedRuntimeServices, }), - { agentId: agent.id }, + { agentId: agent.id, runId: run.id }, ); } catch (err) { await onLog( diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 241355b6..775756e0 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -1,4 +1,5 @@ export { companyService } from "./companies.js"; +export { feedbackService } from "./feedback.js"; export { companySkillService } from "./company-skills.js"; export { agentService, deduplicateAgentName } from "./agents.js"; export { agentInstructionsService, syncInstructionsBundleConfigFromFilePath } from "./agent-instructions.js"; diff --git a/server/src/services/instance-settings.ts b/server/src/services/instance-settings.ts index ccefea7c..f1d2afcd 100644 --- a/server/src/services/instance-settings.ts +++ b/server/src/services/instance-settings.ts @@ -1,6 +1,7 @@ import type { Db } from "@paperclipai/db"; import { companies, instanceSettings } from "@paperclipai/db"; import { + DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, instanceGeneralSettingsSchema, type InstanceGeneralSettings, instanceExperimentalSettingsSchema, @@ -18,10 +19,13 @@ function normalizeGeneralSettings(raw: unknown): InstanceGeneralSettings { if (parsed.success) { return { censorUsernameInLogs: parsed.data.censorUsernameInLogs ?? false, + feedbackDataSharingPreference: + parsed.data.feedbackDataSharingPreference ?? DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, }; } return { censorUsernameInLogs: false, + feedbackDataSharingPreference: DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, }; } diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index b6bdb066..dc62bdc6 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -21,7 +21,7 @@ import { projectWorkspaces, projects, } from "@paperclipai/db"; -import { extractAgentMentionIds, extractProjectMentionIds } from "@paperclipai/shared"; +import { extractAgentMentionIds, extractProjectMentionIds, isUuidLike } from "@paperclipai/shared"; import { conflict, notFound, unprocessable } from "../errors.js"; import { defaultIssueExecutionWorkspaceSettingsForProject, @@ -467,6 +467,28 @@ function withActiveRuns( export function issueService(db: Db) { const instanceSettings = instanceSettingsService(db); + async function getIssueByUuid(id: string) { + const row = await db + .select() + .from(issues) + .where(eq(issues.id, id)) + .then((rows) => rows[0] ?? null); + if (!row) return null; + const [enriched] = await withIssueLabels(db, [row]); + return enriched; + } + + async function getIssueByIdentifier(identifier: string) { + const row = await db + .select() + .from(issues) + .where(eq(issues.identifier, identifier.toUpperCase())) + .then((rows) => rows[0] ?? null); + if (!row) return null; + const [enriched] = await withIssueLabels(db, [row]); + return enriched; + } + function redactIssueComment(comment: T, censorUsernameInLogs: boolean): T { return { ...comment, @@ -883,26 +905,19 @@ export function issueService(db: Db) { return row ?? null; }, - getById: async (id: string) => { - const row = await db - .select() - .from(issues) - .where(eq(issues.id, id)) - .then((rows) => rows[0] ?? null); - if (!row) return null; - const [enriched] = await withIssueLabels(db, [row]); - return enriched; + getById: async (raw: string) => { + const id = raw.trim(); + if (/^[A-Z]+-\d+$/i.test(id)) { + return getIssueByIdentifier(id); + } + if (!isUuidLike(id)) { + return null; + } + return getIssueByUuid(id); }, getByIdentifier: async (identifier: string) => { - const row = await db - .select() - .from(issues) - .where(eq(issues.identifier, identifier.toUpperCase())) - .then((rows) => rows[0] ?? null); - if (!row) return null; - const [enriched] = await withIssueLabels(db, [row]); - return enriched; + return getIssueByIdentifier(identifier); }, create: async ( @@ -1542,7 +1557,11 @@ export function issueService(db: Db) { return comment ? redactIssueComment(comment, censorUsernameInLogs) : null; })), - addComment: async (issueId: string, body: string, actor: { agentId?: string; userId?: string }) => { + addComment: async ( + issueId: string, + body: string, + actor: { agentId?: string; userId?: string; runId?: string | null }, + ) => { const issue = await db .select({ companyId: issues.companyId }) .from(issues) @@ -1562,6 +1581,7 @@ export function issueService(db: Db) { issueId, authorAgentId: actor.agentId ?? null, authorUserId: actor.userId ?? null, + createdByRunId: actor.runId ?? null, body: redactedBody, }) .returning(); diff --git a/ui/src/api/companies.ts b/ui/src/api/companies.ts index 82d2e54e..9529e897 100644 --- a/ui/src/api/companies.ts +++ b/ui/src/api/companies.ts @@ -28,7 +28,14 @@ export const companiesApi = { data: Partial< Pick< Company, - "name" | "description" | "status" | "budgetMonthlyCents" | "requireBoardApprovalForNewAgents" | "brandColor" | "logoAssetId" + | "name" + | "description" + | "status" + | "budgetMonthlyCents" + | "requireBoardApprovalForNewAgents" + | "feedbackDataSharingEnabled" + | "brandColor" + | "logoAssetId" > >, ) => api.patch(`/companies/${companyId}`, data), diff --git a/ui/src/api/issues.ts b/ui/src/api/issues.ts index 7f0b2b27..be38f8ad 100644 --- a/ui/src/api/issues.ts +++ b/ui/src/api/issues.ts @@ -1,6 +1,9 @@ import type { Approval, DocumentRevision, + FeedbackTargetType, + FeedbackTrace, + FeedbackVote, Issue, IssueAttachment, IssueComment, @@ -76,6 +79,26 @@ export const issuesApi = { }), release: (id: string) => api.post(`/issues/${id}/release`, {}), listComments: (id: string) => api.get(`/issues/${id}/comments`), + listFeedbackVotes: (id: string) => api.get(`/issues/${id}/feedback-votes`), + listFeedbackTraces: (id: string, filters?: Record) => { + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(filters ?? {})) { + if (value === undefined) continue; + params.set(key, String(value)); + } + const qs = params.toString(); + return api.get(`/issues/${id}/feedback-traces${qs ? `?${qs}` : ""}`); + }, + upsertFeedbackVote: ( + id: string, + data: { + targetType: FeedbackTargetType; + targetId: string; + vote: "up" | "down"; + reason?: string; + allowSharing?: boolean; + }, + ) => api.post(`/issues/${id}/feedback-votes`, data), addComment: (id: string, body: string, reopen?: boolean, interrupt?: boolean) => api.post( `/issues/${id}/comments`, diff --git a/ui/src/components/CommentThread.tsx b/ui/src/components/CommentThread.tsx index 2501d95a..353074ed 100644 --- a/ui/src/components/CommentThread.tsx +++ b/ui/src/components/CommentThread.tsx @@ -1,12 +1,19 @@ import { memo, useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; import { Link, useLocation } from "react-router-dom"; -import type { IssueComment, Agent } from "@paperclipai/shared"; +import type { + Agent, + FeedbackDataSharingPreference, + FeedbackVote, + FeedbackVoteValue, + IssueComment, +} from "@paperclipai/shared"; import { Button } from "@/components/ui/button"; import { Check, Copy, Paperclip } from "lucide-react"; import { Identity } from "./Identity"; import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector"; import { MarkdownBody } from "./MarkdownBody"; import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor"; +import { OutputFeedbackButtons } from "./OutputFeedbackButtons"; import { StatusBadge } from "./StatusBadge"; import { AgentIcon } from "./AgentIconPicker"; import { formatDateTime } from "../lib/utils"; @@ -38,9 +45,17 @@ interface CommentReassignment { interface CommentThreadProps { comments: CommentWithRunMeta[]; queuedComments?: CommentWithRunMeta[]; + feedbackVotes?: FeedbackVote[]; + feedbackDataSharingPreference?: FeedbackDataSharingPreference; + feedbackTermsUrl?: string | null; linkedRuns?: LinkedRunItem[]; companyId?: string | null; projectId?: string | null; + onVote?: ( + commentId: string, + vote: FeedbackVoteValue, + options?: { allowSharing?: boolean; reason?: string }, + ) => Promise; onAdd: (body: string, reopen?: boolean, reassignment?: CommentReassignment) => Promise; issueStatus?: string; agentMap?: Map; @@ -127,6 +142,11 @@ function CommentCard({ agentMap, companyId, projectId, + feedbackVote = null, + feedbackDataSharingPreference = "prompt", + feedbackTermsUrl = null, + onVote, + voting = false, highlightCommentId, queued = false, }: { @@ -134,6 +154,14 @@ function CommentCard({ agentMap?: Map; companyId?: string | null; projectId?: string | null; + feedbackVote?: FeedbackVoteValue | null; + feedbackDataSharingPreference?: FeedbackDataSharingPreference; + feedbackTermsUrl?: string | null; + onVote?: ( + vote: FeedbackVoteValue, + options?: { allowSharing?: boolean; reason?: string }, + ) => Promise; + voting?: boolean; highlightCommentId?: string | null; queued?: boolean; }) { @@ -218,6 +246,15 @@ function CommentCard({ /> ) : null} + {comment.authorAgentId && onVote && !isQueued && !isPending ? ( + + ) : null} {comment.runId && !isPending ? (
{comment.runAgentId ? ( @@ -247,12 +284,26 @@ const TimelineList = memo(function TimelineList({ agentMap, companyId, projectId, + feedbackVoteByTargetId, + feedbackDataSharingPreference = "prompt", + feedbackTermsUrl = null, + onVote, + votingTargetId, highlightCommentId, }: { timeline: TimelineItem[]; agentMap?: Map; companyId?: string | null; projectId?: string | null; + feedbackVoteByTargetId?: Map; + feedbackDataSharingPreference?: FeedbackDataSharingPreference; + feedbackTermsUrl?: string | null; + onVote?: ( + commentId: string, + vote: FeedbackVoteValue, + options?: { allowSharing?: boolean; reason?: string }, + ) => Promise; + votingTargetId?: string | null; highlightCommentId?: string | null; }) { if (timeline.length === 0) { @@ -299,6 +350,11 @@ const TimelineList = memo(function TimelineList({ agentMap={agentMap} companyId={companyId} projectId={projectId} + feedbackVote={feedbackVoteByTargetId?.get(comment.id) ?? null} + feedbackDataSharingPreference={feedbackDataSharingPreference} + feedbackTermsUrl={feedbackTermsUrl} + onVote={onVote ? (vote, options) => onVote(comment.id, vote, options) : undefined} + voting={votingTargetId === comment.id} highlightCommentId={highlightCommentId} /> ); @@ -310,9 +366,13 @@ const TimelineList = memo(function TimelineList({ export function CommentThread({ comments, queuedComments = [], + feedbackVotes = [], + feedbackDataSharingPreference = "prompt", + feedbackTermsUrl = null, linkedRuns = [], companyId, projectId, + onVote, onAdd, agentMap, imageUploadHandler, @@ -334,6 +394,7 @@ export function CommentThread({ const effectiveSuggestedAssigneeValue = suggestedAssigneeValue ?? currentAssigneeValue; const [reassignTarget, setReassignTarget] = useState(effectiveSuggestedAssigneeValue); const [highlightCommentId, setHighlightCommentId] = useState(null); + const [votingTargetId, setVotingTargetId] = useState(null); const editorRef = useRef(null); const attachInputRef = useRef(null); const draftTimer = useRef | null>(null); @@ -360,6 +421,15 @@ export function CommentThread({ }); }, [comments, linkedRuns]); + const feedbackVoteByTargetId = useMemo(() => { + const map = new Map(); + for (const feedbackVote of feedbackVotes) { + if (feedbackVote.targetType !== "issue_comment") continue; + map.set(feedbackVote.targetId, feedbackVote.vote); + } + return map; + }, [feedbackVotes]); + // Build mention options from agent map (exclude terminated agents) const mentions = useMemo(() => { if (providedMentions) return providedMentions; @@ -463,21 +533,38 @@ export function CommentThread({ } } + async function handleFeedbackVote( + commentId: string, + vote: FeedbackVoteValue, + options?: { allowSharing?: boolean; reason?: string }, + ) { + if (!onVote) return; + setVotingTargetId(commentId); + try { + await onVote(commentId, vote, options); + } finally { + setVotingTargetId(null); + } + } + const canSubmit = !submitting && !!body.trim(); return (

Comments & Runs ({timeline.length + queuedComments.length})

- {timeline.length > 0 ? ( - - ) : null} + {liveRunSlot} @@ -599,6 +686,7 @@ export function CommentThread({
+ ); } diff --git a/ui/src/components/IssueDocumentsSection.tsx b/ui/src/components/IssueDocumentsSection.tsx index 9e2617b0..6b2bab78 100644 --- a/ui/src/components/IssueDocumentsSection.tsx +++ b/ui/src/components/IssueDocumentsSection.tsx @@ -1,6 +1,13 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import type { DocumentRevision, Issue, IssueDocument } from "@paperclipai/shared"; +import type { + DocumentRevision, + FeedbackDataSharingPreference, + FeedbackVote, + FeedbackVoteValue, + Issue, + IssueDocument, +} from "@paperclipai/shared"; import { useLocation } from "@/lib/router"; import { ApiError } from "../api/client"; import { issuesApi } from "../api/issues"; @@ -9,6 +16,7 @@ import { queryKeys } from "../lib/queryKeys"; import { cn, relativeTime } from "../lib/utils"; import { MarkdownBody } from "./MarkdownBody"; import { MarkdownEditor, type MentionOption } from "./MarkdownEditor"; +import { OutputFeedbackButtons } from "./OutputFeedbackButtons"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -101,14 +109,26 @@ function documentHasUnsavedChanges(doc: IssueDocument, draft: DraftState | null) export function IssueDocumentsSection({ issue, canDeleteDocuments, + feedbackVotes = [], + feedbackDataSharingPreference = "prompt", + feedbackTermsUrl = null, mentions, imageUploadHandler, + onVote, extraActions, }: { issue: Issue; canDeleteDocuments: boolean; + feedbackVotes?: FeedbackVote[]; + feedbackDataSharingPreference?: FeedbackDataSharingPreference; + feedbackTermsUrl?: string | null; mentions?: MentionOption[]; imageUploadHandler?: (file: File) => Promise; + onVote?: ( + revisionId: string, + vote: FeedbackVoteValue, + options?: { allowSharing?: boolean; reason?: string }, + ) => Promise; extraActions?: ReactNode; }) { const queryClient = useQueryClient(); @@ -207,6 +227,15 @@ export function IssueDocumentsSection({ }); }, [documents]); + const feedbackVoteByTargetId = useMemo(() => { + const map = new Map(); + for (const feedbackVote of feedbackVotes) { + if (feedbackVote.targetType !== "issue_document_revision") continue; + map.set(feedbackVote.targetId, feedbackVote.vote); + } + return map; + }, [feedbackVotes]); + const hasRealPlan = sortedDocuments.some((doc) => doc.key === "plan"); const isEmpty = sortedDocuments.length === 0 && !issue.legacyPlanDocument; const newDocumentKeyError = @@ -718,6 +747,7 @@ export function IssueDocumentsSection({ const displayedRevisionNumber = selectedHistoricalRevision?.revisionNumber ?? doc.latestRevisionNumber; const displayedUpdatedAt = selectedHistoricalRevision?.createdAt ?? doc.updatedAt; const showTitle = !isPlanKey(doc.key) && !!displayedTitle.trim() && !titlesMatchKey(displayedTitle, doc.key); + const canVoteOnDocument = Boolean(doc.latestRevisionId && doc.updatedByAgentId && !doc.updatedByUserId && onVote); return (
+ {canVoteOnDocument && doc.latestRevisionId ? ( + + onVote?.(doc.latestRevisionId!, vote, options) ?? Promise.resolve() + } + /> + ) : null} ) : null} diff --git a/ui/src/components/OutputFeedbackButtons.tsx b/ui/src/components/OutputFeedbackButtons.tsx new file mode 100644 index 00000000..9d26a022 --- /dev/null +++ b/ui/src/components/OutputFeedbackButtons.tsx @@ -0,0 +1,259 @@ +import { useEffect, useState } from "react"; +import type { FeedbackDataSharingPreference, FeedbackVoteValue } from "@paperclipai/shared"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ThumbsDown, ThumbsUp } from "lucide-react"; +import { cn } from "../lib/utils"; + +export function OutputFeedbackButtons({ + activeVote, + disabled = false, + sharingPreference = "prompt", + termsUrl = null, + onVote, +}: { + activeVote?: FeedbackVoteValue | null; + disabled?: boolean; + sharingPreference?: FeedbackDataSharingPreference; + termsUrl?: string | null; + onVote: (vote: FeedbackVoteValue, options?: { allowSharing?: boolean; reason?: string }) => Promise; +}) { + const [pendingVote, setPendingVote] = useState<{ + vote: FeedbackVoteValue; + reason?: string; + keepReasonPromptOpen?: boolean; + } | null>(null); + const [isSaving, setIsSaving] = useState(false); + const [downvoteReason, setDownvoteReason] = useState(""); + const [collectingDownvoteReason, setCollectingDownvoteReason] = useState(false); + const [downvoteAllowSharing, setDownvoteAllowSharing] = useState(undefined); + const [optimisticVote, setOptimisticVote] = useState(null); + const visibleVote = optimisticVote ?? activeVote ?? null; + + useEffect(() => { + if (optimisticVote && activeVote === optimisticVote) { + setOptimisticVote(null); + } + }, [activeVote, optimisticVote]); + + async function submitVote( + vote: FeedbackVoteValue, + options?: { allowSharing?: boolean; reason?: string }, + behavior?: { keepReasonPromptOpen?: boolean }, + ) { + setIsSaving(true); + try { + await onVote(vote, options); + setPendingVote(null); + if (!behavior?.keepReasonPromptOpen) { + setCollectingDownvoteReason(false); + setDownvoteReason(""); + setDownvoteAllowSharing(undefined); + } + } catch (error) { + setOptimisticVote(null); + throw error; + } finally { + setIsSaving(false); + } + } + + function beginVote( + vote: FeedbackVoteValue, + reason?: string, + behavior?: { keepReasonPromptOpen?: boolean }, + ) { + if (sharingPreference === "prompt") { + setPendingVote({ + vote, + ...(reason ? { reason } : {}), + ...(behavior?.keepReasonPromptOpen ? { keepReasonPromptOpen: true } : {}), + }); + return; + } + const allowSharing = sharingPreference === "allowed"; + if (vote === "down") { + setDownvoteAllowSharing(allowSharing); + } + void submitVote( + vote, + { + ...(allowSharing ? { allowSharing: true } : {}), + ...(reason ? { reason } : {}), + }, + behavior, + ); + } + + function handleVote(vote: FeedbackVoteValue) { + setOptimisticVote(vote); + if (vote === "down") { + setCollectingDownvoteReason(true); + setDownvoteReason(""); + setDownvoteAllowSharing(undefined); + void beginVote("down", undefined, { keepReasonPromptOpen: true }); + return; + } + void beginVote(vote); + } + + return ( + <> +
+ + +
+ {collectingDownvoteReason ? ( +
+
What could have been better?
+